2011-11-21

Linux 的 udev 是個有趣又有用的玩具(4)

在前幾個單元,我們把一個 Bourne-shell script my_script 掛勾於(hook up to) /etc/udev/rules.d/z99_test.rules,故而每次 uevent 產生時,my_script 都會被執行。這個機制可以用以下幾行清楚地表示:
$ cat /etc/udev/rules.d/z99_test.rules
RUN+="/home/nobody/my_script"
$ ls -l ~/my_script
-rwxr-xr-x 1 nobody nobody 1056 Nov 22 03:04 /home/nobody/my_script
在這個單元裡,我們要討論 my_script 裡在圖形介面環境下開啟視窗(pop-up windows)時,所可能碰到的問題(issues)。

讓我們首先再回到一個 my_script 的基本架構(basic structure):

#!/bin/sh
# my_script example 4-1
LOGFILE=/tmp/my_script.log
exec 3>> $LOGFILE && exec >& 3 && exec 2>&1 # redirect stdout/stderr to LOGFILE
SND_INSERT="/xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav"
SND_REMOVE="/xp/WINDOWS/Media/chimes.wav"
PROG="`basename $0`/$SEQNUM"

if [ "$SUBSYSTEM" = "usb_device" -a "$ACTION" = "add" ]
then
    echo ""
    echo "$PROG: $SUBSYSTEM $ACTION $DEVNAME"
    echo "$PROG: aplay -q $SND_INSERT &"
    aplay -q "$SND_INSERT" &

    echo "$PROG: exiting..."
    exit 0

elif [ "$SUBSYSTEM" = "usb_device" -a "$ACTION" = "remove" ]
then
    echo ""
    echo "$PROG: $SUBSYSTEM $ACTION $DEVNAME"
    echo "$PROG: aplay -q $SND_REMOVE &"
    aplay -q "$SND_REMOVE" &

    echo "$PROG: exiting..."
    exit 0
else
    exit 0
fi

為了使 LOGFILE 清晰易讀(readability),我們把變數 PROG 定義為這個程式的名稱(script name)再加上每個 uevent 的序列號 SEQNUM。由於我們把 aplay(1) 放到背景去執行(backgrounding),aplay(1) 這個 process 是與 my_script 分開來(detached)同時(concurrently)進行。所以,如果聲音檔(sound file)很長的話,my_script 可能會在 aplay(1) 尚未撥放完畢時就提前結束(terminate)。在背景執行的 aplay(1) 以及 my_script 的 process ID's 可以在程式裡,分別以 special variables "$!" 以及 "$$" 查詢。我們加上了一行 "echo exiting" 用來突顯 my_script 的結束(terminaton)可能比聲音檔播放完畢更早發生。此外,也不能不注意,要是 uevent 頻繁的話,由於每一個 uevent 都會執行 my_script,所以在系統上,這個程式很可能會有好幾個分身(processes)正在同時執行(concurrently),因為當一個分身還沒有結束之前,很可能又會發生另一個 uevent,而這些分身彼此之間是各自為政的,它們都具有不同的 PID。

編輯好 my_script 之後,必要的話(if necessary),不要忘了:
$ sudo /etc/init.d/udev reload
$ chmod 755 ~/my_script
先清空(clear) logfile 的內容
$ sudo sh -c "echo > /tmp/my_script.log"
再插入(insert)、拔除(remove)任一 USB 裝置,我們就可以 less /tmp/my_script.log 看到類似以下的內容:

my_script/2541: usb_device add /dev/bus/usb/002/011
my_script/2541: aplay -q /xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav &
my_script/2541: exiting...

my_script/2557: usb_device remove /dev/bus/usb/002/011
my_script/2557: aplay -q /xp/WINDOWS/Media/chimes.wav &
my_script/2557: exiting...

接下來,我們就要嘗試在 USB 裝備插入(insert)時,啟動一個視窗(pop-up window),在移除時(remove),則殺掉(kill)這個視窗。我們就以大部份 Linux 平台上都會有的 xeyes(1) 為實驗對象。

#!/bin/sh
# my_script example 4-2
LOGFILE=/tmp/my_script.log
exec 3>> $LOGFILE && exec >& 3 && exec 2>&1 # redirect stdout/stderr to LOGFILE
SND_INSERT="/xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav"
SND_REMOVE="/xp/WINDOWS/Media/chimes.wav"
PROG="`basename $0`/$SEQNUM"

if [ "$SUBSYSTEM" = "usb_device" -a "$ACTION" = "add" ]
then
    echo ""
    echo "$PROG: $SUBSYSTEM $ACTION $DEVNAME"
    echo "$PROG: aplay -q $SND_INSERT &"
    aplay -q "$SND_INSERT" &

    echo "$PROG: xeyes &"
    xeyes &

    echo "$PROG: exiting..."
    exit 0

elif [ "$SUBSYSTEM" = "usb_device" -a "$ACTION" = "remove" ]
then
    echo ""
    echo "$PROG: $SUBSYSTEM $ACTION $DEVNAME"
    echo "$PROG: aplay -q $SND_REMOVE &"
    aplay -q "$SND_REMOVE" &

    echo "$PROG: killall xeyes"
    killall xeyes

    echo "$PROG: exiting..."
    exit 0
else
    exit 0
fi

大家會發現,xeyes(1) 並未啟動。檢視 /tmp/my_script.log 則得到類似以下的訊息:

my_script/2622: usb_device add /dev/bus/usb/002/012
my_script/2622: aplay -q /xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav &
my_script/2622: xeyes &
Error: Can't open display:
my_script/2622: exiting...

my_script/2638: usb_device remove /dev/bus/usb/002/012
my_script/2638: aplay -q /xp/WINDOWS/Media/chimes.wav &
my_script/2638: killall xeyes
xeyes: no process killed
my_script/2638: exiting...

其中 Error: Can't open display: 表示,xeyes(1) 抱怨不知道到底要把視窗顯示到那一個 display 上去,因為 environment variable DISPLAY 並未定義(undefined)。那麼我們就以選項
xeyes -display :0.0
叫它顯示到 localhost 上的第一個顯示器上。然而除此之外,也還會碰到進一步與 display 有關的問題,諸如:
Xlib: connection to ":0.0" refused by server
Xlib: No protocol specified
Error: Can't open display: :0.0
這些問題是由於 my_script 執行的時候,是 root 的權限(permission),然而啟動 X 視窗介面的往往是一般的使用者,例如 "nobody"(也就是我們一直用來代表一個不具有系統管理員權限的一般使用者),所以雖然已經指定了 DISPLAY,仍然會遭遇到 "connection refused by server" 以及 "can't open display" 的問題。對於這些問題,通常是以 xhost(1) 這個指令來給予 root 開啟視窗的權限,例如 "xhost +local:root",可是這個指令也必須要由具有 nobody 的權限的 process 來執行。以下的 my_script 版本(example 4-3)提出一個可行的解決方法,就是以 sudo -u nobody 的方式,來設定這個 process 的權限(permission)。可是我們又不方便把 "nobody" 寫死(hard-wire)在這個 script 裡面,所以就叫 ps(1) 去找出啟動 X window system 圖形介面的 user,把它放在 $XOWNER 這個變數裡,如此一來,就無須每次都要因使用者而異,去修改 "nobody" 這個常數(constant)。寫程式的一般原則之一,是要盡可能避免使用太多的常數(constant)。另一方面,我們也把 $DISPLAY 這個環境變數先定義好,並 export 出去,免得每一次開啟一個視窗,都要寫一次上述的 -display :0.0。所以這個版本會是一個非常有用的參考範例。大家也會注意到,在這個範例裡,並未出現像 "nobody" 這樣的假設。

#!/bin/sh
# my_script example 4-3
LOGFILE=/tmp/my_script.log
exec 3>> $LOGFILE && exec >& 3 && exec 2>&1 # redirect stdout/stderr to LOGFILE
SND_INSERT="/xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav"
SND_REMOVE="/xp/WINDOWS/Media/chimes.wav"
PROG="`basename $0`/$SEQNUM"

if [ "$SUBSYSTEM" = "usb_device" -a "$ACTION" = "add" ]
then
    echo ""
    echo "$PROG: $SUBSYSTEM $ACTION $DEVNAME"
    echo "$PROG: aplay -q $SND_INSERT &"
    aplay -q "$SND_INSERT" &

    # XOWNER=`ps aux | grep -e xinit | fgrep -v grep | awk '{print $1}'`
    XOWNER=`ps -C xinit -o user --no-headers`

    echo "$PROG: export DISPLAY=:0.0"
    export DISPLAY=:0.0

    echo "$PROG: sudo -H -u $XOWNER xhost +local:root"
    sudo -H -u $XOWNER xhost +local:root

    echo "$PROG: xeyes &"
    xeyes &

    echo "$PROG: exiting..."
    exit 0

elif  [ "$SUBSYSTEM" = "usb_device" -a "$ACTION" = "remove" ]
then
    echo ""
    echo "$PROG: $SUBSYSTEM $ACTION $DEVNAME"
    echo "$PROG: aplay -q $SND_REMOVE &"
    aplay -q "$SND_REMOVE" &

    echo "$PROG: killall xeyes"
    killall xeyes

    echo "$PROG: exiting..."
    exit 0
else
    exit 0
fi

如此一來,我們終於得到了預期中的結果:

my_script/2350: usb_device add /dev/bus/usb/001/014
my_script/2350: aplay -q /xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav &
my_script/2350: export DISPLAY=:0.0
my_script/2350: sudo -H -u nobody xhost +local:root
non-network local connections being added to access control list

my_script/2350: xeyes &
my_script/2350: exiting...

my_script/2370: usb_device remove /dev/bus/usb/001/014
my_script/2370: aplay -q /xp/WINDOWS/Media/chimes.wav &
my_script/2370: killall xeyes
my_script/2370: exiting...

其中與最關鍵的 display 權限問題有關的部分,我們已經以紅色標出。相信這是一個網路上還沒有人去好好處理過的問題。關於這個問題,這篇部落格文章可能是目前唯一的一篇。這裡先提供出解決方案,詳細的情形待有空再陸續補上。

後記 2011-12-02

在 my_script 的 example 4-3 裡,我們在 SUBSYSTEM 等於 "usb_device" 而且 ACTION 等於 "remove" 的 case 下,發出一個指令 killall xeyes,來把 xeyes 這個程式 terminate 掉。或許有人會問,為什麼不在啟動的時候記下它的 PID,然後用 kill $PID 來 terminate 掉 xeyes 呢?這個問題很重要。因為雖然 ACTION 有 "add" 或 "remove" 兩種情形,都是在這個 script 裡 handle 的,可是很重要的一點是,這兩種情形並非是同一個 process,而是兩個毫不相干的 processes。也就是說,就算在啟動 xeyes 的當時,用一個變動記下 xeyes 的 PID,在 ACTION 等於 "remove" 的時候,已經是在另外一個 process 了,先前的 PID 早就失去了,因為那個 process 早就 terminated 掉了。除非我們把那個 PID 儲存在系統的其他地方,例如某一個硬碟檔案裡,而如果是這樣,又是別的寫法了。

上一個單元:Linux 的 udev 是個有趣又有用的玩具(3)
下一個單元:Linux 的 udev 是個有趣又有用的玩具 (5) : Smallest Working Bourne-Shell Automounter(世界上最小的 Automounter)

沒有留言:

張貼留言