Thursday, November 04, 2004
我是苦命 patcher
我是苦命 patcher,我想來說說 VSS 的壞話。
在我進公司之前,老闆選了 Microsoft Visual SourceSafe (VSS) 作為部門的 version control system。這,是苦命的開始。理論上,使用 version control system,可以幫助程式員們,彼此協同開發。我以前就有過這樣子的經驗,和學弟兩人,使用 CVS 一起開發一套社群軟體。兩人卯足了勁拼命 commit,彼此分工合作,絲毫不會有程式互相「蓋到」的情況發生。可是,現在在公司上班,與同事協同開發的感覺,完全變了一個樣。在我們部門裡, VSS 根本就只是被當作一個 source deposit,而沒有發揮 source control 應該要有的功能。
在我們部門裡,典型的協同開發的流程,就如同下面這個故事一般:
- A 君寫了一套軟體的初版,程式已經可以動作,初版的功能皆已齊備了,然後他趕緊在 VSS 裡建一個專案,或是找一個相對應的位置,把程式放進去。
- 然 後,老闆找了 B 君來做一個新功能,於是 B 君就找 A 君要程式,A 君說,放在 VSS 裡,於是 B 君就回去自己的電腦前,想要把程式 checkout 出來,卻發現他沒有權限。於是,B 君再回去找 A 君,然後 A 君就打開他的 VSS,加了 B 君的帳號。就在 B 君要回自己位子上去工作前,A 君拉住了 B 君,對 B 君說:「你不要亂 commit 喔,程式寫好了之後,拿給我看看,沒問題再 commit。」
- 於是,B 君便辛勤地與新功能奮鬥。其間,歷經了若干次因當機或其他不明原因,系統 crash,導致程式流失。還好,B 君平時有做備份的習慣,因此在 B 君的電腦裡,最後一共存了十幾份程式的備份檔,其中有約 1/4,因為在備份的時候,忘了標註日期時間,最後變成了讓人根本搞不清楚到底內含了什麼東西的一沱廢物。
- 由 於新功能的解法未定,在開發過程中,為了嘗試各種解 法,B 君的電腦裡的程式目錄,又分成了好幾個不同的目錄,好比 FooProj-Method1、FooProj-Method1+Method2、FooProj-Solution7、FooProj- Method1+Method3、FooProj-Mess、FooProj-HalfSolved 等等讓人有點明瞭又有點模糊的奇怪目錄名。
- 另 外,B 君覺得,有些程式可以抽離出來,成為一個 utility library,好比對檔名的分析與處理等。因此,B 君便將這些程式抽離出來,另外做成了一個 library。然後在原本的程式裡,呼叫這個 library。當然,這個 library,又是放在另外一個目錄裡。
- 很不幸地,可以被抽離出來的程式,其種類越來越多,於是這個 utility library 就越來越龐大。更麻煩的是,這個 utility library 裡,有些功能種類已經寫完了,有些功能種類還沒有寫完,另外還有些功能種類,B 君一改再改,卻仍是還沒有改出覺得可以滿意的版本。也因此,FooProj-Method1 目錄的程式,必須搭配 UtilLib-Initial 目錄下的 utility library,然後 FooProj-Method1+Method2 目錄必須搭配 UtilLib-StrOK 目錄,FooProj-Solution7 目錄必須搭配 UtilLib-StrOK+SQLTemp 目錄,而 FooProj-Method1+Method3 所對應的 utility library 的目錄,已於上次當機而遺失了,FooProj-Mess 搭配 UtilLib-Good1、FooProj-HalfSolved 搭配 UtilLib-Good1+I18nTemp+SQLBad。老實說,連 B 君都快搞不清楚這些目錄彼此之間,錯綜複雜的關係了。
- 總 算,B 君改出了一個可以跑的版本了,於是 B 君將 FooProj-OK1 與 UtilLib-Good2 包成兩個 zip 檔,寄給 A 君,然後來到 A 君的電腦前,與 A 君討論。A 君打開信件,把 FooProj-OK1 裡的所有 .cpp 檔,全部 copy 到自己專案目錄下的 src/ 目錄下,然後 compile 程式,瞬間指著上千個 error message 對 B 君發飆。B 君只好很委屈地對 A 君解釋說,還需要加上 UtilLib-Good2 才行。於是 A 君與 B 君,手忙腳亂地花了好一陣子,才順利地把 UtilLib-Good2 裝進 A 君的電腦裡,並把相關設定都設好。此時,B 君的程式,才能正確地在 A 君的電腦上跑。
- 看 完程式之後,A 君對 B 君的程式,尚稱滿意。不過,因為 B 君的程式,是用 C++ 寫的,A 君對 C++ 並不怎麼熟悉,再加上,A 君不太願意,在他偶爾還會改進的這套程式裡,加上 UtilLib-Good2 這個拖油瓶,因此,A 君對 B 君說:「嗯,不錯,不過你還是先不要 commit 好了,等這個新功能穩定之後,我們再來弄個最終版本。喔,對,這幾天我又稍微改了一下程式,現在程式應該會快了一些,你看是不是要把新的程式 merge 一下。」由於 A 君較為資深,因此 B 君便無異議地答應了。
- 回到自己的電腦前,B 君開始手動 merge 兩份程式。很快地,B 君發現,光用 diff 找出相異處,根本不可行,因為兩人的程式排版風格差異太多了。好比,A 君習慣用 i++,而 B 君則習慣用 ++i;A 君習慣用 if (p) s();,B 君習慣用 if (p) { s(); }。類似這種不算差異的差異,不勝枚舉,B 君改的煩不勝煩,幾乎每一個 diff 回報的差異,都必須用人眼仔細檢視,才能確定該怎麼修改。這次 merge,B 君花了三天才完成,因為第一次 merge 完之後,發現程式跑了會當,而又找不出到底是哪裡漏了,所以 B 君只好又再重新 merge 了一次。
- 這樣的故事,週而復始,不斷地發生……
當然,上面的故事,點出了許多的問題。不過做人處事嘛,最好還是對事不對人。因此,咱們來罵罵 VSS 吧。
在 Alan De Smet 的「Visual SourceSafe: Microsoft's Source Destruction System」這篇文章裡,整理出了許多 VSS 的邪惡之處。另外,Alan De Smet 也介紹了這一篇「Visual SourceSafe Version Control: Unsafe at any Speed?」,不過這篇的排版比較差,看起來很辛苦,所以我就懶得看了。以上當然不是事情的全部,以下,則是我的抱怨:
- VSS 沒有 branch。這可以說是最大的問題,我們部門之所以會把 VSS 當成 source deposit 來用,也正是因為這個原因。為了維持 repository 裡程式的正確性,我們必須要靠人力,把所有問題都解決了之後,才能把程式放進 VSS 裡。當然,專案的第一個起始開發者,也就是 repository 的建立者,有些小小的特權,可以將之當作是他個人專用的 repository,比較不受維持 repository 裡程式的正確性的限制。因此,在這樣的情況下,「維持 repository 裡程式的正確性」這個教條,實際上變成了「維持 repository 建立者電腦裡程式的正確性」。
- 沒有統一的帳號控管機制。VSS 沒有辦法做到,整個部門共用一套帳號系統,然後每個人拿自己的帳號,去存取不同專案的 repository,是專案成員的話,便有讀寫權限,非專案成員的話,便只有唯讀的權限。
- 不同專案之間的整合性極差。在 VSS 裡,根本沒有所謂外部專案這回事,因此在我們部門裡,同樣的 library,同時在好幾個 repositories 裡出現好幾次。當然,這些 library 究竟有沒有什麼差異,那就只有天曉得了。
- 不 支援非 Windows 的 eol-style。VSS 是 Microsoft 的產物,承襲 M$ 的一貫作風,在 VSS 眼裡,是沒有其他作業系統的存在的。因此,本部門的 perl 程式,一律不放進 VSS 裡,因為當 checkout 出來之後,程式裡的 end-of-line 全亂了套,使得 perl 的 regular expression 失效,導致程式出錯。
- 在 VSS 裡,是採用 lock-edit-unlock 這個 model,來共享程式碼的。然而不幸地,VSS 的 lock/unlock 介面,做的很差,與 Visual C++ 的整合,更是慘兮兮,專案一 checkout 出來,根本無法開始做事,因為一切全鎖住了。因此大家在 checkout 專案之後的第一件事,便是在專案根目錄按右鍵,將整個目錄樹的唯讀屬性去掉。再加上,VSS 的使用者介面,很難讓人察覺,其他人改了些什麼。因此,大家都都沒有養成,在 commit 之前要先弄清楚有沒有別人也 commit 過的習慣。也因此,「蓋程式」的事情,依然不斷地發生。
- VSS 的邪惡,是訴諸不盡地……
夜了,剛總算把 2/3 的專案給人工 merge 完畢,剩下一個全域變數不曉得跑到那個檔案去了。我是苦命 patcher,我覺得我比 VSS 還要厲害。-.-||
[Thunderbird]怎樣知道IMAP Server上的信件夾,有哪些有新信?
個人習慣自己架 IMAP Server,透過 fetchmail + procmail + spamassassin 來收信、分信以及擋信。把信全放在 IMAP Server 有一個好處是,到哪裡都可以灌個 MUA 來看信,至不濟還可以用 webmail 遷就一下。
可是,由於信件已經在 server 上事先用 procmail 自動分到不同的信件夾去了,因此,我在看信的時候,便需要知道,哪一個信件夾有新信件。由於會被自動分信的,多半是屬於電子報或郵遞論壇類型的信件,不需 急著看其內容,僅需依據標題,再來決定是否需要花費時間下載觀看。另外,廣告信也是一種其實並不需要下載的信種,不過為了避免誤判,所以還是需要稍微瀏覽 一下標題,因此也屬於這類僅需要下載標題的信種。
在 Outlook Express 裡面就有這樣子的一個功能,可以對每一個 IMAP 信件夾,分別指定其下載的方式,可以為不下載、只下載信頭,以及全部下載等三種方式。透過將有可能會有新信的信件夾,設定成只下載信頭,其他的信件夾,設 定成不下載,然後 INBOX 設定成全部下載,我便可以達到我所希望的效果,在 INBOX 裡的信件,多是確定要看得信件,已經下載完畢直接可以閱讀,而其他的信件夾裡,則是新的電子報或郵遞論壇信件,等我去決定是否要觀看,歸類用的信件夾,則根本不需要下載。
然而,Thunderbird 似乎並無法達到這樣「細緻」的功能。請問有任何網友有類似的經驗與需求,或知道有任何的 extensions 或 extensions 的組合,甚至搭配 server-side 設定的辦法,可以達到我上述的需求嗎?懇請賜教。
--
我的微笑,堅持要有鼻子。:-)
有關此問題的討論:Mozilla@Taiwan
Thursday, October 28, 2004
Anti-idle by Screen and Expect
目標
使用 Screen + Expect,取代 Windows 上一般的 Enhanced Telnet Client 如 PacketSiTE 之流,達到在 server 上真正「一直掛站」的效果。
Client 端不限,PuTTY 是不錯的選擇。
Server 端需要 screen、expect,以及可設定 prompt 的 shell,下面只介紹 tcsh 的作法。
首先,在 screenrc 裡面加上這一行:
hardstatus alwayslastline '%{.bY}[%H]%{.bR} %?%-LW %?%{..W}%n%f %t%{.bR}%?%+LW%?%=%{.bY}%0c'
這 是用來在 terminal 上,使用最底下一行,顯示 screen 的 status line,會顯示的東西,除了 screen 裡各個「window」的 number、flag 及 title等資訊。額外左端加上 server 的 hostname,右端加上 server 的時間。這樣子長的就會有點像 Windows 的工作列,右端擺上 server 時間還有個好處是,由於每分鐘會變動一次,遇到那種 idle 太久會斷線的網路環境裡,從 local 到 server 端的連線就不會斷線,因為每分鐘至少會傳一次資料。
以下統一一下 terminology 好了:使用 PuTTY 這端叫 local 端,跑 screen + expect 的 server 那端叫 server 端,然後連到的 bbs 站叫 remote 端。
另 外要注意的是,目前的 window 會用高亮度顯示,上一個 window,在數字右邊會有一個 - 號。背景 window 有「嗶」聲發生,但我們還沒「轉台」過去看的 window,數字旁邊會有一個 ! 號。這個 ! 號,有助於我們掌握是哪一個 window 的 BBS 有訊息傳來。另外,您最好額外在 screenrc 裡設定:
bell_msg 'Bell in window %n^G'
這 樣 screen 還會在背景 window 有「嗶」發聲時,顯示短暫兩、三秒的訊息,告知是那一個窗有「嗶」聲。這個設定裡頭的 ^G 表示,然後要真的發出「嗶」聲。因為 screen 預設背景 window 的「嗶」是不發出聲音的,故利用 bell_msg 我們可以迫使其發出聲音來。
另外要注意的是,我的 screen 版本為:
SHEEL> screen -v Screen version 3.09.11 (FAU) 14-Feb-02 SHEEL> uname -sr FreeBSD 4.10-PRERELEASE
我 在另一台 Linux(RedHat)上設 hardstatus,底下會顯示亂碼一行。該機器的 screen 版本比我稍舊,詳細版本忘記了。目前還不確定是該版本的 screen 有問題,亦或是 screenrc 編寫的有問題(RedHat 預設了一個複雜的 screenrc)。
由於 hardstatus line 會佔去一行的空間,而一般的 BBS 都至少要是 80 x 24,故原始的 PuTTY 設定便無法容納這額外需要的一行。所以請在 PuTTY 的 Window 設定頁裡,將 Rows 改成 25,以容納這額外多出來的 hardstatus line。
接著,利用 screen 的 dynamic title 機制,送出 title-escape-sequence 以根據 remote bbs 名稱修改 screen window title。為了要在 remote bbs 斷線之後,能夠把 screen window title 修改回來,所以一併參照 screen 的文件,替 shell 也應用上這個 dynamic title 機制。
Screen 的 dynamic title 的原理為:當畫面顯示了一個 title-escape-sequence (
k ) 時,就會於這個 sequence 後面,尋找某個指定的字串。從這個字串的後面一個字元開始,到第一個空白字元出現為止,這中間的字串,就是會被設定為 screen window title 的字串。整個動作,會發生在 title-escape-sequence 後的第一個 \n 出現時。而要讓 screen 照上述這麼做,我們必須要指定這一個「某個指定的字串」,以及若該 screen window title 的字串不存在(指定字串之後馬上碰上 \n) 時,所要顯示的字串。設定方法便是,在 screenrc 裡,設定 shelltitle 為 'SEARCH|NAME',其中,SEARCH 就是那個「某個指定的字串」,而 NAME 就是若該字串不存在時,所要顯示的東西。配合 prompt 這個 shell 指令,反映到 screen window title 上來。tcsh + screen 的設定方法如下: 在 tcshrc 裡設定:
set prompt = "%{\ek\e\\%}%% "
我自己的 prompt 會複雜一點,但總之,在最後面加上上面這個字串就對了。然後在 screenrc 裡設定:
shelltitle '% |TCSH'
這樣子,,screen 就會抓取每個 shell 指令,反映到 screen window title來。試打個 top 指令,就可以從剛剛在第 3 步裡所設定的 hardstatus line 上面,體驗到 dynamic title 的效果了。
接著,我們要編寫 except 檔,來達到「上 BBS 不會斷線」,以及「依據所上的 BBS 站,修改 screen window title」的功能。Expect 程式如下:
#!/usr/local/bin/expect -f # # Name: bbs_noidle.expect # Description: expect script to run a shell which avoids being # "autologged out" by sending a
every # specified timeout limit if no other I/O has occurred. # Author: Don Libes, December 30, 1991 # Modified by: Clive Lin to fit tw BBS. # bbs_noidle.expect telnet bbs.some.where # And send only # Modified by: Jeff Hung @ 20040829 to add # these features: # - Use zh-telnet to connect to BBS. # - Able to use abbrevation instead of complete hostname. # - Use screen's 'title-escape-sequence' to dynamically # set window title. # set timeout 180 set anti_idle "\177"; log_user 0 system stty -echo raw if {[llength $argv] <= 0} { send "Usage: bbs_noidle.expect ( | )\n"; exit 0; } set bbs_abbr "$argv"; set bbs_host "$argv"; switch $bbs_abbr { lady { set bbs_host "bbs.lady.idv.tw" } tkucs { set bbs_host "bbs.cs.tku.edu.tw" } nctuiim { set bbs_host "bbs.iim.nctu.edu.tw" } tku { set bbs_host "bbs.tku.edu.tw" } ptt { set bbs_host "ptt.cc" } zoo { set bbs_host "zoo.twbbs.org" } } # Cause screen to print where we're connecting to. send "We're connecting to \033k\033\\% $bbs_abbr %\n\r"; eval spawn -noecho /usr/local/bin/zh-telnet $bbs_host expect { timeout { send $anti_idle; exp_continue } -re .+ { send_user -raw -- $expect_out(buffer); exp_continue } -i $user_spawn_id -re .+ { send -- $expect_out(buffer); exp_continue } } 傳 給這個 expect 程式的命令列參數,可以是在程式中的 switch {} 裡頭所列的 host name abbrevation,或是正確完整可傳給 telnet 當作命令列參數的的 host name。修個這裡的列表,您就可以編寫您自己的 bbs list。
其中的秘訣在於這一行:
send "We're connecting to \033k\033\\% $bbs_abbr %\n\r";
在 連線前,先送出 title-escape-sequence 修改 screen window title,達到顯示目前所上的 BBS 站的效果。在程式最開頭的 set timeout 可以設定「防呆」的時間為多少秒。 而 set anti_idle 則可以設定,防呆時要送出那個字串。一般來說,送出 \177,也就是 DELETE 字元,差不多就很夠用了。
補充一下,在 .cshrc 裡頭加上:
if ( ! $?WINDOW ) then alias screen 'env LC_CTYPE=en_US.ISO8859-1 screen -x -RR' else alias screen 'env LC_CTYPE=en_US.ISO8859-1 screen' endif |
然後在 .login 裡加上:
if ( ! $?WINDOW ) then screen endif |
就可以在 login 後自動進入原先的 screen,進入仍在掛站中的 BBS。當然,這樣子離線就不能真的打 exit 離線,要用 screen 的 detach 離線法:C-a D D。
若 是有要在 shell 裡工作的話,請建立一個同 UID 的帳號,好比叫 jeffbbs,在 PuTTY 裡建立不同名稱的連線,預設使用 jeffbbs 登入,這樣子的話,就可以在用 jeffbbs 登入時,享用 Screen + Expect 掛站大法。而在使用 jeffhung 登入時,則是進入正常的 shell。這是因為 screen 是用 login id 來判定 session 歸屬的。
Simple DB3 C++ Wrapper
為了寫程式方便順手包的 db3++.h,將操作 Berkeley DB ver.3[1]的 C API 稍作包裝,以便在 C++ 裡更好用。因為純粹以快速寫完為原則,所以一些成員函式的 prototype 設計就沒有仔細構思,error handling 也未盡完善,不過以下所展示的已足以說明如何包裝、使用 db3:
#include |
Wednesday, October 20, 2004
[Perl] Escape Full Big5 Character for R.E. in Perl under 5.8
由於 perl 5.8 以下(不包括 5.8),其字串是 byte 的組成單位是 byte 而非 character,所以碰上內含有 regular expression 特殊字元的 big5 字,就會造成 regular expression 判斷出錯。例如:
print (($s =~ m/會議/) ? 'match' : 'no match'); |
不管 $s 是否含有「會議」一詞,皆會印出 'match'。這是因為「會」字的第二個 byte,是個 '|',而 '|' 在 perl regular expression 裡,代表著特殊的意義。這個問題的通常解法,是將含有 regular expression 特殊字元的全形 Big5 字元,改寫成以 hex 數字表示的形式。例如,m/會議/ 就可以被改寫成 m/[\xB7][\x7C]議/。不過,如果是程式裡面的 regular expression 那還好,見一個改一個便是。可是,若要 match 的字串是 run-time 才能知道的,好比說 user input 的話,這招就沒效了。
這問題的終極且最好的解法,當然是換用 perl 5.8。然而,換用 perl 5.8,可能代表著整套程式都必須跟著升級,而這通常會是很麻煩的一件事,而且,也不一定每一台機器都有灌 perl 5.8。因此,我們需要另一個方法來迴避這個問題。
要迴避這個問題,其實策略也很簡單:「(只)檢查每一個全形 big5 字元,若其內含有 r.e. 特殊字元,便將之代換成以 hex 數字表示的形式」。因此,我們有了以下的 function:
my $__FULLBIG5CHARS = q{ # Table adopted from http://freebsd.sinica.edu.tw/zh-tut/big5.html # (http://netlab.cse.yzu.edu.tw/~statue/freebsd/zh-tut/big5.html) # # 第一位元組 第二位元組 字區 制訂 # ---------------------------------------------------------------------------------------------------- ([\xA1-\xA2][\x40-\x7E\xA1-\xFE])| # 各種符號區 Big5_1984 ( [\xA3][\x40-\x7E\xA1-\xBF])| # 各種符號區 (?]括標點符號、ASCII 全形符號、注音符號等) Big5_1984 ( [\xA3] [\xE1])| # 歐元符號 CP950 ([\xA4-\xC5][\x40-\x7E\xA1-\xFE])| # 常用字區 Big5_1984 ( [\xC6] [\x40-\x7E])| # 常用字區 Big5_1984 ( [\xC6] [\xA1-\xFE])| # 罕用符號區 Big5_ETen ( [\xC7][\x40-\x7E\xA1-\xFE])| # 罕用符號區 (?]括日文、俄文等) Big5_ETen ( [\xC8][\x40-\x7E\xA1-\xD3])| # 罕用符號區 (?]括俄文、輸入法特殊符號等) Big5_ETen ([\xC9-\xF8][\x40-\x7E\xA1-\xFE])| # 次常用字區 Big5_1984 ( [\xF9][\x40-\x7E\xA1-\xD5])| # 次常用字區 Big5_1984 ( [\xF9] [\xD6-\xDC])| # 七個擴充字 (碁, 銹, 裏, 墻, 恒, 粧, 嫺) Big5_ETen ( [\xF9] [\xDD-\xFE])| # 表格符號區 Big5_ETen }; sub mmi_escape_big5_re { my ($mbcs) = @_; die if (!defined($mbcs)); # One character per list element. my @chars = (); while ($mbcs =~ /\G($__BIG5CHARS)/gox) { push @chars, $&; } my $str = ''; for my $char (@chars) { if (length($char) > 1) { if ($char =~ m/[\[|\^\$]/o) { my $c0 = sprintf('[\x%X]', ord(substr($char, 0, 1))); my $c1 = sprintf('[\x%X]', ord(substr($char, 1, 1))); $char = "$c0$c1"; } else { } } $str .= $char; } return $str; } |
使用這個 function 很簡單,在用之前,把要用來 match 的變數,用這個 function 先「過濾」一遍就可以了。例如:
$content = 'blah~ blah~ blah~'; while ($input = <>) { chomp($input); $re = mmi_escape_big5_re($input); if ($content =~ m/$re/) { print "There are '$input' in \$content.\n"; } } |
Subscribe to Posts [Atom]