2011-12-21 15 views
13

Sto lavorando a uno script che è diventato così complesso che voglio includere un'opzione semplice per aggiornarlo alla versione più recente. Questo è il mio approccio:È un approccio di autoaggiornamento valido per uno script 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 
} 

Lo script sembra funzionare come previsto, ma mi chiedo se ci potrebbe essere avvertimenti con questo tipo di approccio. Ho solo difficoltà a credere che uno script possa sovrascrivere se stesso senza ripercussioni.

Per essere più chiaro, mi chiedo, se, forse, bash sarebbe leggere ed eseguire lo script linea per linea e dopo il mv, il exit 0 potrebbe essere qualcos'altro dal nuovo script. Penso di ricordare che Windows si comporta in questo modo con i file .bat.

Aggiornamento: il mio snippet originale non include set -o errexit. A mio parere, questo dovrebbe tenermi al sicuro da problemi causati da wget.
Inoltre, in questo caso, UPDATE_BASE punta a una posizione sotto il controllo della versione (per alleviare i dubbi).

Risultato: Sulla base del contributo di queste risposte, ho costruito questo nuovo approccio:

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 
} 
+0

non sarebbe meglio sostituire il 'exit 0 'con' exec "$ 0" 'più eventuali argomenti che è necessario trasmettere (forse:' exec "$ 0" "$ @" ')?Cioè, dopo aver affrontato la meccanica del download, verificando e sostituendo la precedente incarnazione dello script come discusso da @shellter. –

+0

@ JonathanLeffler Sì, forse. Per ora, volevo mantenere questa azione separata e breve in modo che non interferisse con il resto della sceneggiatura. Una volta che avrò fiducia nell'approccio, potrei estenderlo. –

+0

Si noti che ottenere le autorizzazioni con stat richiede un diverso flag e formato su osx. L'equivalente è 'stat -f '% A' $ 0'. – meonlol

risposta

5

(Almeno non cerca di continuare l'esecuzione dopo l'aggiornamento stesso!)

La cosa che mi rende nervoso per il tuo approccio è che si sta sovrascrivendo lo script corrente (mv $0.tmp $0) come è in esecuzione . Ci sono un certo numero di ragioni per cui questo sarà probabilmente lavoro, ma non scommetterei grandi quantità che è garantito che funzioni in tutte le circostanze.Non conosco nulla in POSIX o in qualsiasi altro standard che specifichi in che modo la shell elabora un file che sta eseguendo come script.

Ecco cosa probabilmente accadrà:

Si esegue lo script. Il kernel vede la riga #!/bin/sh (non l'hai mostrata, ma presumo sia lì) e invoca /bin/sh con il nome del tuo script come argomento. La shell usa quindi fopen(), o forse open() per aprire lo script, legge da esso e inizia a interpretarne il contenuto come comandi di shell.

Per uno script sufficientemente piccolo, la shell probabilmente legge l'intera cosa in memoria, in modo esplicito o come parte del buffer eseguito dal normale I/O di file. Per uno script più grande, potrebbe leggerlo in blocchi mentre è in esecuzione. Ma in entrambi i casi, probabilmente apre il file solo una volta e lo mantiene aperto finché è in esecuzione.

Se si rimuove o rinomina un file, il file effettivo non viene necessariamente cancellato immediatamente dal disco. Se c'è un altro collegamento, o se qualche processo lo ha aperto, il file continua ad esistere, anche se potrebbe non essere più possibile per un altro processo aprirlo con lo stesso nome, o del tutto. Il file non viene cancellato fisicamente fino a quando l'ultimo collegamento (voce della directory) che si riferisce ad esso è stato rimosso, e, nessun processo lo ha aperto. (Anche allora, il suo contenuto non saranno immediatamente cancellati, ma che sta andando al di là di ciò che è rilevante qui.)

E inoltre, il comando mv che clobbers il file di script è immediatamente seguito da exit 0.

MA è almeno immaginabile che la shell possa chiudere il file e quindi riaprirlo per nome. Non riesco a pensare a nessuna buona ragione per farlo, ma non so assolutamente che non lo farà.

E alcuni sistemi tendono a bloccare i file più rigorosi della maggior parte dei sistemi Unix. Su Windows, ad esempio, sospetto che il comando mv fallirebbe perché un processo (la shell) ha il file aperto. Il tuo script potrebbe fallire su Cygwin. (Non l'ho provato)

Quindi ciò che mi rende nervoso non è tanto la piccola possibilità che potrebbe fallire, ma la lunga e tenue linea di ragionamento che sembra dimostrare che probabilmente riuscirà, e il possibilità molto reale che ci sia qualcos'altro a cui non ho pensato.

Il mio suggerimento: scrivere un secondo script il cui unico e unico lavoro è aggiornare il primo. Inserisci la funzione runSelfUpdate() o codice equivalente in quello script. Nello script originale, utilizzare exec per richiamare lo script di aggiornamento, in modo che lo script originale non sia più in esecuzione quando lo si aggiorna. Se si desidera evitare il fastidio di mantenere, distribuire e installare due script separati. potresti avere lo script originale creare lo script di aggiornamento con univoco in /tmp; questo risolverebbe anche il problema dell'aggiornamento dello script di aggiornamento. (Non mi preoccuperei di ripulire lo script di aggiornamento generato automaticamente in /tmp, si riaprirebbe lo stesso tipo di worm.)

+0

Questo è esattamente quello a cui stavo pensando. E voglio mantenere tutto in 1 file a tutti i costi, quindi penso che andrò con un approccio in 2 fasi. –

4

Sì, ma ... mi sento di raccomandare di mantenere una versione più strati della storia di script, a meno che l'host remoto può anche eseguire il controllo della versione con le cronologie. Detto questo, per rispondere direttamente al codice che hai postato, vedi i seguenti commenti ;-)

Cosa succede al tuo sistema quando wget ha un blocco, sovrascrive in silenzio parte del tuo script di lavoro con solo un parziale o altrimenti corrotto copia? Il tuo prossimo passo è un mv $0.tmp $0 così hai perso la versione funzionante. (Spero che tu abbia nel controllo della versione sul telecomando!)

È possibile controllare per vedere se wget restituisce dei messaggi di errore

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

Inoltre, regola-of-thumb test aiuteranno, cioè

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

ma sono difficilmente infallibili.

Se il tuo $ 0 potrebbe accumularsi con spazi al suo interno, meglio circondare tutti i riferimenti come "$0".

di essere super-prova di proiettile, considerano controllando tutti i ritorni di comando e che Octal_Mode ha un valore ragionevole

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 

Spero che questo aiuta.

+0

Grazie mille. Questo è tutto un input molto prezioso. Ma non ero troppo preoccupato per i download difettosi nel momento in cui ho scritto la domanda. Quindi mi stavo principalmente chiedendo come bash elaborerà la sceneggiatura. Aggiornerò la mia domanda di conseguenza. –

0

Molto tardi rispondi qui, ma come ho appena risolto anche questo, ho pensato che potesse aiutare qualcuno di inviare l'approccio:

#!/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 
Problemi correlati