2011年12月20日 星期二

[Linux] 實作在centos裡插入USB自動執行某個script

Part 1: register 一個callback script


本文的重點不(not)在於如何撰寫(writing)或修改 udev 規則(rules),也不在於如何更改新增裝置的名稱及權限。而在於,脫離 udev rules 的特殊語法(syntax),另外以我們已經熟悉的 Bourne-shell-script 來設計當週邊設備有所異動時,我們想要的反應及動作,例如,發出聲響(sound effects),跳出視窗(pop-up windows),或是自動 mount 之後,備份特定檔案(file backup)等。

如果你在指令行下
$ ps -e | grep udev
可以看到 udevd 的話,就表示說,你的 Linux 系統上已經有一個所謂的 udev deamon 正在管理核心(kernel)裡所傳送出來的 uevents。這時,/dev/ 資料夾裡的檔案是動態配置的(dynamically polulated),譬如說,如果系統上沒有 sdg 這個 block device,那麼在 /dev/ 裡也不會有 sdg 這個檔案。如果你的 kernel 版本(可以 uname -r 查詢)是 >= 2.6.13 的話,那麼你就可以跟著本文一步一步地來玩一些有趣的實驗,這可能包括像是插入(insert)或拔出(unplug)一個隨身碟(thumb-drive)或一張記憶卡(SD card)時,讓電腦發出你所要的聲音(sound),或是跳出一個視窗(pop-up window),或者是自動 mount 一個 partition,或是甚至於在 mount 之後自動播放影音或是自動瀏覽照片(media-players)。雖然我們不建議像 autorun.bat 這種極具安全疑慮(security issues)的功能,但是這自然也是很容易辦到的。

因為我個人所使用的是 kernel 版本 2.6.27.59 上的 Debian Etch 或 Lenny,所以我們接下來就以此平台為基礎,開始做一些簡單有趣的實驗。

我們先以 root 的權限在 /etc/udev/rules.d/ 這個資料夾裡建立一個新檔:
$ sudo vim /etc/udev/rules.d/z100_run_my_script.rules
檔案內容只要一行:
RUN+="/home/my_script"
my_script 是放在 HOME 裡的任何一個可執行的程式。我們將用 shell-script 來示範這個程式如何被 udev 執行。其實在你建好  z100_run_my_script.rules  之後,udev 就已經開始執行 my_script 這個程式了。雖然此刻它還不存在,但是 udev 系統在找不到這個檔案的時候也不會抱怨。每次有電腦周邊元件異動時(uevent),例如,usb device 插入或拔出時,udev 系統都會去 /etc/udev/rules.d/ 資料夾裡依序查看並執行所有的 rules,當輪到我們剛剛才建立的  z100_run_my_script.rules  時,它便會去執行 my_script。

接下來我們便要來寫個叫做 my_script 的程式:
$ cd ~/
$ vim my_script
裡面的內容就先放兩行就好了:
#!/bin/sh
sudo /home/rs232
其中第一行是 UNIX 標準的 shell-script "shebang",它是用來告訴系統,這個 script 是要由 /bin/sh 來執行。第二行則呼叫一個程式出來執行。儲存這個檔案,再做(必要!):
$ chmod 755 my_script
然後先測試一下,是否有執行:
$ ./my_script
再來,你就可以拿任何一個 usb 隨身碟插進去,試試看囉!你每次一插進去一個隨身碟,就會去執行rs232

如果發生原因不明的問題的話,可以試試看:
$ sudo /etc/init.d/udev reload
我們剛剛建立了 /etc/udev/rules.d/ z100_run_my_script.rules  之後,沒有執行以上的 reload,是因為它是新建立的規則檔案(new rule files),但是如果這個檔案的內容經過修改的話,則必須執行以上的 reload,修改過的內容才會生效。

如果讓 my_script 的執行指令在背景執行的話,例如:
#!/bin/sh
sudo /home/rs232 &
這個指令被放到背景執行(backgrounding)之後,這個程式就結束了,而 udev 系統很快地再次執行這一行指令時,也會一再地放到背景去執行……,不能清楚地算出是被執行了幾次。

Part2: 如何加入debug msg到script

我們已經建立起一個機制(mechanism),這包含一個所有 uevent 的萬能勾(universal hook),也就是 /etc/udev/rules.d/ z100_run_my_script.rules ,以及讓這個勾勾直接跳過來執行(execute)、放在家裡面(HOME)的一個叫做 my_script 的程式。也就是:
$ cat /etc/udev/rules.d/ z100_run_my_script.rules
RUN+="/home/my_script"
$ ls -l ~/my_script
-rwxr-xr-x 1 home home 1056 Nov 22 03:04 /home/my_script
因為這個程式執行的時候並沒有 tty,也就是說,它不能夠印出任何我們看得到的訊息(messages),所以我們就用 sudo 來執行一個檔案,來通知(notify)我們這個程式的執行狀況,例如,到底有沒有被執行到、被執行了幾次,或是在什麼情況下會被執行。但是為了寫一個有用的程式,我們終究必須要讓這個程式印出可以看得到的訊息,以便監控程式發展過程的任何問題。


所以在這個單元,我們就必須要先建立好程式發展環境,讓我們可以鉅細靡遺地監控這個程式的執行細節,也讓我們可以 debug 寫程式時無法迴避的許多錯誤。沒有 tty 怎麼辦呢?一個簡單的辦法就是讓 my_script 這個程式把所有的訊息輸出到一個 logfile,例如:
#!/bin/sh
LOGFILE=/tmp/my_script.log
sudo /home/rs232
date >> $LOGFILE
這裡 ">>" 的意思,就是把 date 這個指令所產生的 stdout 都附加(append) 到 $LOGFILE 裡。但是 ">>" 並不包括 stderr。所以萬一系統找不到 date(1) 這個程式的話,例如,你把 "date" 錯打成 "data",在 $LOGFILE 裡還是看不到像是
bash: data: command not found
的錯誤訊息,因為,像上列這種錯誤訊息,通常是輸出到 stderr,而 ">>" 只 redirect 了 stdout。

我們現在就先檢查一下,到底 $LOGFILE 有沒有真的被產生出來、檔案屬性如何、檔案裡面到底有沒有東西:
$ ls -l /tmp/my_script.log
-rw-r--r-- 1 root root 348 Nov 4 22:10 /tmp/my_script.log
$ less /tmp/my_script.log

請注意,它所有權屬於 root,這一點很重要,因為,雖然我們的程式 my_script 屬於一個一般的使用者(user permission),例如 "home",然而它是由 udev 系統叫出來執行的,所以這個 process 也跟著具有 root 的權限(permission),而其所產生的檔案,自然也具有 root 的預設權限 644。我們可以再看一次 my_script 這個檔案的屬性:
$ ls -l ~/my_script
-rwxr-xr-x 1 home home 188 Nov 4 22:23 my_script
接下來,為了可以在 $LOGFILE 裡記錄下來這個程式所產生的所有錯誤訊息(error messages),我們在下一個範例,故意漏打 "home" 的 "e",讓 sudo 找不到這個檔,而發出送到 stderr 的錯誤訊息。同時,在這一行最後面加上 "2>> $LOGFILE"。這個意思是說,把所有的 stderr 重新導向(redirect),並附加(append)到 $LOGFILE 這個檔案裡。
#!/bin/sh
LOGFILE=/tmp/my_script.log
sudo /hom/rs232 2>> $LOGFILE
date >> $LOGFILE
修改好以上的 my_script 並儲存好新版之後,再插上或拔除任何一個 usb 隨身碟,就應該可以在 $LOGFILE 裡邊找到 sudo 那一行所產生的錯誤訊息。因為這一行執行有誤(syntax error),所以這一次我們看不到任何反映。

為了避免必須在每一個指令後面都必須加上 ">> $LOGFILE" 或是 "2>> $LOGFILE",我們要介紹一個一勞永逸的方法,來把這個程式所產生的所有 stdout 以及 stderr 都 redirect 到 $LOGFILE 裡。修改 my_script 成為:
#!/bin/sh
LOGFILE=/tmp/my_script.log
exec 3>> $LOGFILE && exec >& 3 && exec 2>&1
sudo /home/rs232
date 
以上新版 my_script 的第 3 行,會把從第四行開始,所產生的所有 stdout 以及 stderr 都重新導向(redirect),並附加(append)到 $LOGFILE 裡去。自然,這一行必須放在所有可能會輸出文字的指令之前。

我們很快地就會發現,logfile 裡面所記錄的資料,在先後順序有一點混亂(scrumbled order)。這是因為 uevent 是很頻繁(frequent)發生的事件,而每一次的 uevent 都會觸發 my_script 的執行,所以常常同一個時間(concurrently)會有 my_script 的許多分身(processes)同時、但不同步地(asynchronously)在執行,而它們都正在寫入這個 logfile,彼此之間會形成一種競爭(competitive)的態勢,導致所寫入資訊的前後順序交錯(disorder),以至於看起來好像並沒有按照 my_script 裡的執行順序輸出。其實,順序交錯的輸出是導因於 my_script 的許多不同分身,以非同步的競爭狀況交錯寫入的結果。因此,udev 系統每次呼叫我們的 my_script 時,都會設定一個環境變數 SEQNUM 來顯示該事件(uevent)的產生順序。事實上,udev 系統執行 my_script 時,會設定更多對有用的變數(defined variables)。這可以用 udevmonitor(8) 加上 --env 的選項來監看。


Part 3: 寫一個偵測USB插入script


插入一個 USB 隨身碟時,系統上會產生許多次的 uevents。拔除時亦然。我們再複習一下這個簡單的掛勾機制(hook):

$ cat /etc/udev/rules.d/ z100_run_my_script.rules
RUN+="/home/my_script"
$ ls -l ~/my_script
-rwxr-xr-x 1 home home 1056 Nov 22 03:04 /home/my_script

為了篩選出(match)方便與之掛勾的特定事件(uevent),我們必須更進一步追蹤這些事件的內幕。首先,把 my_script 改寫為以下 3 行:

#!/bin/sh
echo $SEQNUM $SUBSYSTEM $ACTION $DEVNAME >> /tmp/my_script.log
exit $?

先以 root 的權限把 logfile /tmp/my_script.log 的內容清空,再插入,或拔除手邊任何一個 USB 隨身碟。然後再看看在 /tmp/my_script.log 這個檔案裡面,我們到底收集到了那些情資。結果大概會很接近如下所示的內容:
2124 usb add /dev/1-6
2125 usb add
2126 scsi_host add
2127 usb_device add /dev/bus/usb/001/007
2134 bdi add
2128 scsi add
2129 scsi_disk add
2131 scsi change
2130 scsi_device add
2132 block add /dev/sda
2133 block add /dev/sda1
2136 scsi_disk remove
2135 scsi_device remove
2137 block remove /dev/sda1
2140 scsi remove
2143 usb_device remove /dev/bus/usb/001/007
2138 bdi remove
2141 scsi_host remove
2142 usb remove
2139 block remove /dev/sda
2144 usb remove /dev/1-6
其中第一個 field 是環境變數 SEQNUM 的值,可用來判別事件發生的先後順序,第二個是 SUBSYSTEM,第三個是 ACTION,第四個則為 DEVNAME 的值,其中,DEVNAME 有時候並未設定。細數後可知,插入 USB 的動作導致系統上一共產生了 11 次的 uevents,拔除時,則產生了 10 次的 uevents。在這眾多的 uevents 當中,特別適合用來唯一識別 USB 裝置插入(insert)及拔除(remove)動作的事件,也就是只發生過一次的事件可說是:

SUBSYSTEM==usb_device
ACTION==add
以及
SUBSYSTEM==usb_device
ACTION==remove
以下這個 my_script 示範如何限縮這個 script 的反應,只在插入或移除一個 USB device 時,而且分別只執行一次:

case 1:

#!/bin/sh
LOGFILE=/tmp/my_script.log # 定義變數 LOGFILE
exec 3>> $LOGFILE && exec >& 3 && exec 2>&1 # 把 stdout/stderr 導入 $LOGFILE
USB_INSERT="/home/rs232"

if [ "$SUBSYSTEM" = "usb_device" -a "$ACTION" = "add" ]
then
    echo ""
    echo "=============================="
    date "+%G-%m-%dT%H:%M:%S %z"
    echo "$SUBSYSTEM $ACTION"
    echo "sudo -q \"$USB_INSERT\""
    sudo -q "$USB_INSERT"
fi
exit $?

以case 1在執行時會發現無反應看/tmp/my_script.log時會看到

            sudo :sorry,you must have a tty to run sudo

所以要把 sudo 改權限

            sudo bash

script也要修改

case 2:

#!/bin/sh

LOGFILE=/tmp/my_script.log # 定義變數 LOGFILE

exec 3>> $LOGFILE && exec >& 3 && exec 2>&1 # 把 stdout/stderr 導入 $LOGFILE

if [ "$SUBSYSTEM" = "usb_device" -a "$ACTION" = "add" ]

then


    echo ""

    echo "=============================="

    date "+%G-%m-%dT%H:%M:%S %z"

    echo "$SUBSYSTEM $ACTION"

    ./home/rs232
fi
exit $?

編輯 my_script 的時候,要確認聲音檔是不是存在。編輯完畢之後,要記得做
$ chmod 755 ~/my_script
以上的 script 是說,當(if) SUBSYSTEM 這個變數的值等於(=)usb_device,而且(-a)當 變數 ACTION 的值為 add 的時候,就(then)執行一些 echo(1), date(1), sudo(1) 的指令。請注意,方括號 [ 及 ] 跟 = 的前後,要有空白(space),變數(variable)的前後,也必須加上雙引號(double quotation marks),因為變數值可能會包含空白字元(embedded spaces)。

最後一個指令 "exit $?",是要結束這個 script 並把當時的執行結果傳回(return)給呼叫這個 script 的任何程式。想要省略掉的話,也沒什麼問題,然而我們必須平時就養成撰寫程式的正確習慣,在 UNIX 系統上,任何程式、函式都必須要有傳回值,就算是不知道有誰會去用它。我們不希望看到那一天,當有人突然去用它的時候,就必須面對噩運。這看似小事一件,然而缺乏持續性對小節的正確習慣或態度。


做完以上的 script,只要隨手找個 USB 設備插入,或拔出,就都會執行。這是採用正面表列只當變數 SUBSYSTEM 內傳過來的值為 usb_device 時,才依變數 ACTION 的值是 add 或是 remove 來採取行動。


此篇文章參考
random notes

沒有留言:

張貼留言