2011-12-21 32 views
13

Tôi đang làm việc trên một tập lệnh đã trở nên phức tạp đến nỗi tôi muốn đưa vào một tùy chọn dễ dàng để cập nhật nó lên phiên bản mới nhất. Đây là cách tiếp cận của tôi:Đây có phải là phương pháp tự cập nhật hợp lệ cho tập lệnh bash không?

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 
} 

Kịch bản dường như hoạt động như dự định, nhưng tôi tự hỏi liệu có thể cẩn thận với cách tiếp cận này không. Tôi chỉ có một thời gian khó tin rằng một kịch bản có thể tự ghi đè lên mà không có bất kỳ hậu quả nào.

Để rõ ràng hơn, tôi tự hỏi, nếu, có thể, bash sẽ đọc và thực thi tập lệnh từng dòng và sau mv, thì exit 0 có thể là thứ khác từ tập lệnh mới. Tôi nghĩ rằng tôi nhớ Windows hoạt động như vậy với các tệp .bat.

Cập nhật: Đoạn mã gốc của tôi không bao gồm set -o errexit. Theo sự hiểu biết của tôi, điều đó sẽ giữ cho tôi an toàn khỏi các vấn đề gây ra bởi wget.
Ngoài ra, trong trường hợp này, UPDATE_BASE trỏ đến một vị trí trong điều khiển phiên bản (để giảm bớt lo ngại).

quả: Dựa trên những thông tin từ những câu trả lời, tôi xây dựng cách tiếp cận này được sửa đổi:

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

nó sẽ không thể tốt hơn để thay thế 'thoát 0' với 'exec "$ 0"' cộng với bất kỳ đối số bạn cần để chuyển tiếp (có thể: 'exec "$ 0" "$ @" ')?Đó là, sau khi giao dịch với các cơ chế tải xuống, xác minh và thay thế hóa thân trước đó của tập lệnh như được thảo luận bởi @shellter. –

+0

@JonathanLeffler Có, có thể. Bây giờ, tôi muốn giữ một hành động riêng biệt và ngắn gọn để nó không thể can thiệp vào phần còn lại của kịch bản. Khi tôi tự tin vào cách tiếp cận này, tôi có thể mở rộng nó. –

+0

Lưu ý rằng việc cho phép với stat yêu cầu một cờ và định dạng khác trên osx. Tương đương là 'stat -f'% A '$ 0'. – meonlol

Trả lời

5

(Ít nhất là nó không cố gắng để tiếp tục chạy sau khi cập nhật bản thân!)

Điều đó làm cho tôi lo lắng về cách tiếp cận của bạn là bạn đang ghi đè lên các tập lệnh hiện tại (mv $0.tmp $0) vì nó đang chạy . Có một số lý do tại sao điều này sẽ có thể là công việc, nhưng tôi sẽ không đặt cược số tiền lớn mà nó được đảm bảo hoạt động trong mọi trường hợp.Tôi không biết bất cứ điều gì trong POSIX hoặc bất kỳ tiêu chuẩn nào khác chỉ rõ cách trình bao xử lý một tệp mà nó thực thi dưới dạng tập lệnh.

Đây là những gì có thể sẽ xảy ra:

Bạn thực thi tập lệnh. Hạt nhân nhìn thấy dòng #!/bin/sh (bạn đã không hiển thị nó, nhưng tôi đoán nó có) và gọi /bin/sh với tên của kịch bản của bạn như là một đối số. Sau đó, vỏ sẽ sử dụng fopen() hoặc có thể open() để mở tập lệnh của bạn, đọc từ đó và bắt đầu diễn giải nội dung của tập lệnh dưới dạng các lệnh trình bao.

Đối với tập lệnh đủ nhỏ, trình bao có thể chỉ đọc toàn bộ nội dung vào bộ nhớ, hoặc rõ ràng hoặc là một phần của bộ đệm được thực hiện bởi tệp I/O thông thường. Đối với một kịch bản lớn hơn, nó có thể đọc nó trong khối khi nó thực hiện. Nhưng một trong hai cách, nó có lẽ chỉ mở tập tin một lần, và giữ nó mở miễn là nó thực hiện.

Nếu bạn xóa hoặc đổi tên tệp, tệp thực sự không nhất thiết phải xóa ngay lập tức khỏi đĩa. Nếu có một liên kết cứng khác với nó, hoặc nếu một số tiến trình mở nó, tệp sẽ tiếp tục tồn tại, mặc dù nó có thể không còn khả thi cho một tiến trình khác để mở nó dưới cùng một tên, hoặc là tất cả. Tệp không bị xóa về thể chất cho đến khi liên kết cuối cùng (mục nhập thư mục) đề cập đến nó đã bị xóa, không có quy trình nào mở nó. (Thậm chí sau đó, nội dung của nó sẽ không ngay lập tức bị xóa, nhưng điều đó đang diễn ra ngoài những gì liên quan ở đây.)

Và hơn nữa, lệnh mv rằng clobbers file kịch bản được ngay lập tức sau đó exit 0.

NHƯNG ít nhất có thể hiểu rằng vỏ có thể đóng tệp và sau đó mở lại theo tên. Tôi không thể nghĩ ra bất kỳ lý do chính đáng nào để làm như vậy, nhưng tôi biết không có sự đảm bảo tuyệt đối nào cho việc đó.

Và một số hệ thống có xu hướng khóa tập tin chặt chẽ hơn mà hầu hết các hệ thống Unix thực hiện. Trên Windows, ví dụ, tôi nghi ngờ rằng lệnh mv sẽ thất bại vì một quá trình (shell) có tệp đang mở. Kịch bản của bạn có thể thất bại trên Cygwin. Vì vậy, những gì làm cho tôi lo lắng không phải là quá nhiều khả năng nhỏ mà nó có thể thất bại, nhưng dòng dài và kỳ hạn của lý luận rằng dường như chứng minh rằng nó có thể sẽ thành công, và các khả năng rất thực tế là có điều gì khác mà tôi chưa từng nghĩ tới.

Đề xuất của tôi: viết tập lệnh thứ hai có một công việc duy nhất là cập nhật tệp đầu tiên. Đặt hàm runSelfUpdate() hoặc mã tương đương, vào tập lệnh đó. Trong tập lệnh gốc của bạn, hãy sử dụng exec để gọi tập lệnh cập nhật để tập lệnh gốc không còn chạy khi bạn cập nhật. Nếu bạn muốn tránh những rắc rối của việc duy trì, phân phối và cài đặt hai tập lệnh riêng biệt. bạn có thể có tập lệnh gốc tạo tập lệnh cập nhật với mã duy nhất trong /tmp; điều đó cũng sẽ giải quyết được vấn đề cập nhật tập lệnh cập nhật. (Tôi sẽ không lo lắng về việc dọn dẹp tập lệnh cập nhật tự động trong /tmp; mà chỉ có thể mở lại cùng một hộp giun.)

+0

Đây chính xác là những gì tôi đang nghĩ đến. Và tôi muốn giữ tất cả mọi thứ trong 1 tệp bằng mọi giá, vì vậy tôi nghĩ tôi sẽ đi với một phương pháp 2 bước. –

4

Có, nhưng ... tôi sẽ khuyên bạn giữ một phiên bản lớp hơn về lịch sử của kịch bản của bạn, trừ khi máy chủ từ xa cũng có thể thực hiện kiểm soát phiên bản bằng lịch sử. Điều đó đang được nói, để trả lời trực tiếp mã bạn đã đăng, hãy xem các nhận xét sau ;-)

Điều gì xảy ra với hệ thống của bạn khi wget bị nấc cục, lặng lẽ ghi đè một phần của tập lệnh làm việc của bạn chỉ một phần hoặc bị hỏng sao chép? Bước tiếp theo của bạn là mv $0.tmp $0, do đó bạn đã mất phiên bản làm việc của mình. (Tôi hy vọng bạn có nó trong điều khiển phiên bản trên điều khiển từ xa!)

Bạn có thể kiểm tra xem nếu wget trả bất kỳ thông báo lỗi

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

Ngoài ra, Quy tắc-of-thumb xét nghiệm sẽ giúp đỡ, tức là

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

nhưng hầu như không dễ dàng.

Nếu $ 0 của bạn có thể cuộn tròn với khoảng trống trong đó, tốt hơn là bao quanh tất cả các tham chiếu như "$0".

Để trở thành siêu đạn bằng chứng, xem xét kiểm tra tất cả lợi nhuận lệnh VÀ rằng Octal_Mode có một giá trị hợp lý

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 

Tôi hy vọng điều này sẽ giúp.

+0

Cảm ơn rất nhiều. Đó là tất cả những đầu vào rất có giá trị. Nhưng tôi không quá lo lắng về việc tải xuống bị lỗi vào lúc tôi viết câu hỏi. Vì vậy, tôi đã phần lớn tự hỏi về cách bash sẽ xử lý kịch bản. Tôi sẽ cập nhật câu hỏi của tôi cho phù hợp. –

0

Câu trả lời rất muộn, nhưng tôi cũng đã giải quyết vấn đề này để gửi cách tiếp cận:

#!/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 
Các vấn đề liên quan