Cosa succede se si modifica uno script durante l'esecuzione?


31

Ho una domanda generale, che potrebbe essere il risultato di un malinteso su come i processi vengono gestiti in Linux.

Per i miei scopi, definirò uno 'script' come un frammento di codice bash salvato in un file di testo con permessi di esecuzione abilitati per l'utente corrente.

Ho una serie di script che si chiamano l'un l'altro in tandem. Per semplicità, li chiamerò script A, B e C. Lo script A esegue una serie di istruzioni e quindi fa una pausa, quindi esegue lo script B, quindi fa una pausa, quindi esegue lo script C. In altre parole, la serie di passaggi è qualcosa del genere:

Esegui script A:

  1. Serie di dichiarazioni
  2. Pausa
  3. Esegui script B
  4. Pausa
  5. Esegui script C

So per esperienza che se eseguo lo script A fino alla prima pausa, quindi apporto modifiche allo script B, tali modifiche si riflettono nell'esecuzione del codice quando consento che riprenda. Allo stesso modo se apporto modifiche allo script C mentre lo script A è ancora in pausa, quindi gli consento di continuare dopo aver salvato le modifiche, tali modifiche si riflettono nell'esecuzione del codice.

Ecco la vera domanda quindi, c'è un modo per modificare lo script A mentre è ancora in esecuzione? O la modifica è impossibile una volta iniziata l'esecuzione?


2
penso che dipende dalla shell. sebbene affermi che stai usando bash. sembra che dipenda dal modo in cui la shell carica gli script internamente.
strugee,

il comportamento potrebbe anche cambiare se si fonte il file invece di eseguirlo.
strugee,

1
Penso che bash legga un intero script in memoria prima di eseguirlo.
w4etwetewtwet,

2
@handuel, no non lo fa. Come se non aspettasse che tu digiti "esci" al prompt per iniziare a interpretare i comandi che hai inserito.
Stéphane Chazelas,

1
@StephaneChazelas Sì, la lettura dal terminale non lo fa, tuttavia è diverso dall'esecuzione di uno script.
w4etwetewtwet,

Risposte:


21

In Unix, la maggior parte degli editor funziona creando un nuovo file temporaneo contenente i contenuti modificati. Quando il file modificato viene salvato, il file originale viene eliminato e il file temporaneo viene rinominato con il nome originale. (Esistono, ovviamente, varie garanzie per prevenire la perdita di dati.) Questo è, ad esempio, lo stile utilizzato da sedo perlquando viene invocato con il -iflag ("sul posto"), che non è affatto "sul posto". Avrebbe dovuto essere chiamato "nuovo posto con il vecchio nome".

Funziona bene perché unix assicura (almeno per i filesystem locali) che un file aperto continua a esistere fino alla sua chiusura, anche se viene "eliminato" e viene creato un nuovo file con lo stesso nome. (Non è un caso che la chiamata di sistema unix per "eliminare" un file sia in realtà chiamata "unlink".) Quindi, in generale, se un interprete della shell ha un file sorgente aperto e tu "modifichi" il file nel modo sopra descritto , la shell non vedrà nemmeno le modifiche poiché ha ancora il file originale aperto.

[Nota: come per tutti i commenti basati su standard, quanto sopra è soggetto a interpretazioni multiple e ci sono vari casi angolari, come NFS. I pedanti sono invitati a riempire i commenti con eccezioni.]

È ovviamente possibile modificare direttamente i file; non è solo molto conveniente per scopi di modifica, perché mentre puoi sovrascrivere i dati in un file, non puoi eliminare o inserire senza spostare tutti i dati seguenti, il che implicherebbe un sacco di riscrittura. Inoltre, mentre facevi questo spostamento, il contenuto del file sarebbe imprevedibile e i processi con il file aperto ne risentirebbero. Per evitarlo (come per esempio con i sistemi di database), è necessario un sofisticato set di protocolli di modifica e blocchi distribuiti; cose che vanno ben oltre lo scopo di una tipica utility di modifica dei file.

Quindi, se vuoi modificare un file mentre viene elaborato da una shell, hai due opzioni:

  1. È possibile aggiungere al file. Questo dovrebbe sempre funzionare.

  2. È possibile sovrascrivere il file con nuovi contenuti esattamente della stessa lunghezza . Questo potrebbe funzionare o meno, a seconda che la shell abbia già letto o meno quella parte del file. Poiché la maggior parte degli I / O dei file coinvolge i buffer di lettura e poiché tutte le shell che conosco leggono un intero comando composto prima di eseguirlo, è abbastanza improbabile che tu riesca a cavartela. Certamente non sarebbe affidabile.

Non conosco alcuna formulazione nello standard Posix che in realtà richiede la possibilità di aggiungere un file di script mentre il file è in esecuzione, quindi potrebbe non funzionare con ogni shell conforme a Posix, tanto meno con l'attuale offerta di quasi- e shell a volte conformi a posix. Quindi YMMV. Ma per quanto ne so, funziona in modo affidabile con Bash.

Come prova, ecco un'implementazione "senza loop" del famigerato programma 99 bottiglie di birra in bash, che usa ddper sovrascrivere e aggiungere (la sovrascrittura è presumibilmente sicura perché sostituisce la linea attualmente in esecuzione, che è sempre l'ultima riga del file, con un commento esattamente della stessa lunghezza; l'ho fatto in modo che il risultato finale potesse essere eseguito senza il comportamento di auto-modifica.)

#!/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}   #

Quando eseguo questo, inizia con "Non più", quindi continua a -1 e nei numeri negativi a tempo indeterminato.
Daniel Hershcovich,

Se lo faccio export beer=100prima di eseguire lo script, funziona come previsto.
Daniel Hershcovich,

@DanielHershcovich: assolutamente giusto; test sciatti da parte mia. Penso di averlo risolto; ora richiede un parametro di conteggio opzionale. Una soluzione migliore e più interessante sarebbe ripristinare automaticamente se il parametro non corrisponde alla copia memorizzata nella cache.
rici,

18

bash fa molto per assicurarsi che legge i comandi appena prima di eseguirli.

Ad esempio in:

cmd1
cmd2

La shell leggerà lo script per blocchi, quindi probabilmente leggerà entrambi i comandi, interpreterà il primo e poi tornerà alla fine dello cmd1script e rileggerà nuovamente lo script per leggerlo cmd2ed eseguirlo.

Puoi verificarlo facilmente:

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

(anche se guardando l' straceoutput su questo, sembra che faccia alcune cose più fantasiose (come leggere i dati più volte, cercare indietro ...) rispetto a quando ho provato lo stesso qualche anno fa, quindi la mia affermazione sopra su come cercare indietro potrebbe non si applica più alle versioni più recenti).

Se tuttavia scrivi la tua sceneggiatura come:

{
  cmd1
  cmd2
  exit
}

La shell dovrà leggere fino alla chiusura }, memorizzarla in memoria ed eseguirla. A causa di ciò exit, la shell non legge più dallo script, quindi è possibile modificarlo in modo sicuro mentre la shell lo sta interpretando.

In alternativa, quando si modifica lo script, assicurarsi di scrivere una nuova copia dello script. La shell continuerà a leggere quella originale (anche se viene eliminata o rinominata).

Per fare questo, rinominare the-scriptper the-script.olde copiare the-script.oldper the-scripte modificarlo.


4

Non c'è davvero alcun modo sicuro per modificare lo script mentre è in esecuzione perché la shell può usare il buffering per leggere il file. Inoltre, se lo script viene modificato sostituendolo con un nuovo file, le shell in genere leggono il nuovo file solo dopo aver eseguito determinate operazioni.

Spesso, quando uno script viene modificato durante l'esecuzione, la shell finisce per riportare errori di sintassi. Ciò è dovuto al fatto che, quando la shell chiude e riapre il file di script, utilizza l'offset di byte nel file per riposizionarsi al ritorno.


4

Puoi aggirare questo problema impostando una trappola sul tuo script e quindi utilizzando execper raccogliere il nuovo contenuto dello script. Nota, tuttavia, la execchiamata avvia lo script da zero e non da dove è arrivato nel processo in esecuzione, quindi lo script B verrà chiamato (in seguito).

#! /bin/bash

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

trap reexec 1

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

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

Ciò continuerà a visualizzare la data sullo schermo. Potrei quindi modificare il mio script e passare datea echo "Date: $(date)". Scrivendo che lo script in esecuzione mostra ancora solo la data. Tuttavia, se invio il segnale che ho impostato trapper l'acquisizione, lo script exec(sostituisce il processo in esecuzione corrente con il comando specificato) che è il comando $CMDe gli argomenti $@. È possibile farlo emettendo kill -1 PID- dove PID è il PID dello script in esecuzione - e l'output cambia per mostrare Date:prima datedell'output del comando.

È possibile memorizzare lo "stato" del proprio script in un file esterno (in dire / tmp) e leggere i contenuti per sapere dove "riprendere" sul momento in cui il programma viene rieseguito. È quindi possibile aggiungere un'ulteriore terminazione trap (SIGINT / SIGQUIT / SIGKILL / SIGTERM) per cancellare quel file tmp in modo che quando si riavvia dopo aver interrotto lo "Script A", inizierà dall'inizio. Una versione stateful sarebbe simile 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

Ho risolto questo problema catturando $0e $@all'inizio dello script e usando invece quelle variabili in exec.
Drav Sloan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.