十月 282011
 

這篇文章說明如何用 Plurk API 2.0 自己寫一個噗浪機器人

(本文同步發表於 噗浪官方部落格)

噗浪大部分的機器人仍然使用 Plurk 1.0 撰寫,甚至有一部分機器人使用的是非官方的 API,這篇文章將簡介如何使用 Plurk API 2.0 OAuth 的方式開發機器人程式,同時,這個範例也使用 Plurk Realtime API (Comet Push) 的方式來追蹤時間軸,避免給伺服器帶來過多的負擔

由於 Plurk API 2.0 使用 OAuth 方式,所以啟動機器人之前,您必須先獲得以下四個參數

  • App Key
  • App Secret
  • Access Token
  • Access Token Secret

獲得 App Key 及 App Secret 的方式很簡單,首先先註冊一個新的噗浪帳號用來跑這個機器人,然後登入新帳號後,開啟以下這個連結:

http://www.plurk.com/PlurkApp/

按下「註冊新的應用服務」,填寫關於你的程式的資料,其中 OAuth callback 保持空白即可

註冊完畢後就可以看到你的應用程式列表

然後按下「編輯」這個按鈕,就可以看到以下畫面:

這邊我們就可以得到 App Key 以及 App Secret 了

接下來要進行 OAuth 的授權驗證來取得 Access Token 及 Access Token Secret

按下「測試工具」來開啟 OAuth 的 Test Console

首先按下「Get Request Token」來取得暫時的 Request Token,
接下來按下「Open Authorization URL」來開啟授權頁面:

按下「是,我要授權」後,會得到一個認證碼:

把這個六位數的認證碼記下來,然後回到 Test Console,
按下「Get Access Token」,這時會提示您輸入認證碼

把您剛剛記下來的數字填進去,按下「確定」後,
就可以得到永久有效的 Access Token 及 Access Token Secret 了

然後你就可以開始寫程式了,下面是一個用 Python 寫的噗浪機器人,
把其中 APP_KEY, APP_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET 置換掉即可

這個程式需要使用到 plurk-oauth 這個 Python library,
請把 plurk_oauth/ 這個目錄下的檔案下載回來跟你的程式放在一起就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/python
# -*- coding:utf-8 -*-
 
import re
import json
import urllib2
 
from PlurkAPI import PlurkAPI
 
plurk = PlurkAPI('APP_KEY', 'APP_SECRET')
plurk.authorize('ACCEESS_TOKEN', 'ACCESS_TOKEN_SECRET')
 
comet = plurk.callAPI('/APP/Realtime/getUserChannel')
comet_channel = comet.get('comet_server') + "&new_offset=%d"
jsonp_re = re.compile('CometChannel.scriptCallback\((.+)\);\s*');
new_offset = -1
while True:
    plurk.callAPI('/APP/Alerts/addAllAsFriends')
    req = urllib2.urlopen(comet_channel % new_offset, timeout=80)
    rawdata = req.read()
    match = jsonp_re.match(rawdata)
    if match:
        rawdata = match.group(1)
    data = json.loads(rawdata)
    new_offset = data.get('new_offset', -1)
    msgs = data.get('data')
    if not msgs:
        continue
    for msg in msgs:
        if msg.get('type') == 'new_plurk':
            pid = msg.get('plurk_id')
            content = msg.get('content_raw')
            if content.find("hello") != -1:
                plurk.callAPI('/APP/Responses/responseAdd',
                              {'plurk_id': pid,
                               'content': 'world',
                               'qualifier': ':' })

執行方式:

# python my-robot.py

這個範例程式作的事情很簡單,就是一個無窮迴圈,首先每次都會先接受所有成為朋友的請求,然後看看有沒有新的噗,如果有新的噗,而且內容有 ‘hello’ 字串的話,就會自動回覆一個 ‘world’ 字串

提醒大家,噗浪並不反對機器人的存在,但使用這個範例程式請注意以下幾點:

  1. 建議使用新的帳號,不要用原有的帳號
  2. 機器人請勿主動去加網友為朋友
  3. 機器人請勿去關注(追蹤)任何其他網友
  4. 請勿自動回覆未成為機器人的朋友所發的噗
  5. 請勿張貼廣告訊息
  6. 請注意回覆頻率,以不過度干擾使用者的方式為原則
 Posted by at 17:04
三月 022009
 

最近已經快滿三年的 Thinkpad X60 跑得越來越慢,又常常當機,所以找個比較有空的時間就把它重灌 Windows XP Professional,順便作一下筆記

灌好作業系統後,可以先把 ThinkVantage System Update 裝起來,這樣 Thinkpad 相關的驅動程式及軟體都可以直接從這邊更新回來。對了,記得不要裝 Client Security Solution 因為裝了真的是自找麻煩! 另外 System Migration Assistant 和 Rescue and Recovery 我也沒裝..

接下來就是安裝應用程式了,儘可能都以免費的軟體為主:

PicPick 是一個很強大的截圖軟體
XnView 看圖及簡單的圖形處理
Paint.NET 是一個短小精幹的繪圖軟體
FileZilla 支援 FTP 及 SFTP 的檔案傳輸軟體
7-Zip 檔案解壓縮工具
K-Lite Codec Pack 播放各種影音檔案
Adobe Reader 閱讀 PDF 文件

Alcohol 52% Free Edition 虛擬光碟 (裝免費版的即可, 可選擇不裝贊助軟體)
Avira AntiVir 掃毒軟體
Unlocker 強制解鎖
RocketDock 像蘋果的工具列
新酷音輸入法 (最新版: 0.3.4.8)

其他網路工具包含:

Google 提供的工具:

另外微軟的 PowerToys for Windows XP 也有幾個不錯的小工具:

  • CmdHere
  • PowerToy Calculator
  • Tweak UI

還有之前微軟有提供一個 Microsoft Chinese Date and Time,是一個我很愛用的農民曆及世界時間工具,但現在微軟似乎把連結拿掉了,不過網路上搜尋一下就找到了

 Posted by at 22:53
十一月 112008
 

This is a simple Mantis Mail Gateway for mail-in tickets.
It can be used for alert management for monitoring system.

For installation, add the line below to /etc/aliases:

bug: “|/usr/local/bin/mantis-mail-gateway.pl   PROJECT   REPORTER"

and then run newaliases.

The incoming mail will be posted to a Mantis project named ‘PROJECT’ on behave of user ‘REPORTER’. The mail subject will become the bug summary and the mail content will become the bug description.

Note that this script does not understand MIME multipart encoding and may have problems on mail contents besides plain-ASCII encoding.

You are welcome to add more features.

mantis-mail-gateway.pl: (can be downloaded here)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#!/usr/bin/perl
 
# mantis-mail-gateway.pl
 
# Mantis Mail Gateway
# Shen Cheng-Da (cdsheen AT gmail.com)
# require DBI to connect MySQL database
# http://blog.urdada.net/2008/11/11/95/
 
use DBI;
use POSIX qw(strftime);
 
my $db_host    = 'localhost';
my $db_name    = 'mantis';
my $db_user    = 'monitor';
my $db_pass    = 'monitorpass';
 
my $debug      = 0;
 
my $db = "dbi:mysql:dbname=${db_name};host=${db_host}";
 
die "Usage: $0 [project-name] [reporter]\n" unless @ARGV > 1;
 
my $project_name  = $ARGV[0];
my $reporter_name = $ARGV[1];
 
my $dbh;
my $sql;
my $sth;
my $project  = -1;
my $reporter = -1;
 
$dbh = DBI->connect($db, $db_user, $db_pass)
        || die 'ERROR: '.$dbh->errstr;
 
$sql = "SELECT id,name FROM mantis_project_table
        WHERE name LIKE '$project_name'";
 
$sth = $dbh->prepare($sql) || die 'ERROR: '.$dbh->errstr;
$sth->execute() || die 'ERROR: '.$dbh->errstr;
while( @data = $sth->fetchrow_array() ) {
        $project = $data[0];
}
$sth->finish;
 
die "ERROR: project \`$project_name' does not exist\n" unless $project > 0;
 
print "project: $project_name ($project)\n" if $debug;
 
$sql = "SELECT id,username FROM mantis_user_table
        WHERE username = '$reporter_name'";
 
$sth = $dbh->prepare($sql) || die 'ERROR: '.$dbh->errstr;
$sth->execute() || die 'ERROR: '.$dbh->errstr;
while( @data = $sth->fetchrow_array() ) {
        $reporter = $data[0];
}
$sth->finish;
 
die "ERROR: user \`$reporter_name' does not exist\n" unless $reporter > 0;
 
print "reporter: $reporter_name ($reporter)\n" if $debug;
 
my $subject = '';
my $content = '';
 
while(<STDIN>) {
        s/\s+$//;
        last if $_ eq '';
        $subject = $1 if /^Subject: (.+)$/;
}
while(<STDIN>) {
        $content .= $_;
}
 
$sql = 'INSERT INTO mantis_bug_text_table (description) VALUES (?)';
 
$sth = $dbh->prepare($sql) || die 'ERROR: '.$dbh->errstr;
$sth->execute($content) || die 'ERROR: '.$dbh->errstr;
$sth->finish;
 
my $textid = $dbh->{ q{mysql_insertid} };
 
print "bug text id: $textid\n" if $debug;
 
$sql = 'INSERT INTO mantis_bug_table
                ( project_id, reporter_id,
                  date_submitted, last_updated,
                  bug_text_id, summary )
        VALUES (?,?,?,?,?,?)';
 
my $now = strftime('%Y-%m-%d %H:%M:%S', localtime(time));
 
$sth = $dbh->prepare($sql) || die 'ERROR: '.$dbh->errstr;
$sth->execute($project, $reporter, $now, $now, $textid, $subject)
        || die 'ERROR: '.$dbh->errstr;
$sth->finish;
 
$dbh->disconnect;
 Posted by at 23:09
七月 302008
 

很多人寫完程式要 commit 的時候會偷懶不寫 log,導致有時候要追問題時,
很難得知到底別人改了什麼,這時候裝個 pre-commit hook 還是蠻有用的

下面這個 pre-commit hook 只是很簡單的不允許空白或不含字母的 commit log,
如果需要的話可以很容易擴充加上更多的判斷.. 記得要 chmod 755 喔

[Subversion pre-commit hook to reject commit with empty log]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/perl
 
# pre-commit hook to reject commit with empty log
# remember to chmod 755 on this file
 
die "Usage: $0 [REPOS] [TXN]\n" unless @ARGV > 1;
 
$REPOS=$ARGV[0];
$TXN=$ARGV[1];
 
$svnlook = '/usr/local/bin/svnlook';
 
chomp($author=`$svnlook author -t $TXN $REPOS`);
chomp($log=`$svnlook log -t $TXN $REPOS`);
 
if( $log eq '' || $log =~ /^\W+$/ ) {
    die "\nHello, $author. Empty commit log is not permitted!\n";
}
 
exit(0);

除此之外,對於 pre-commit hook,我還加上了檢查不允許跨 branch 的 commit,
因為一旦有了跨 branch 的 commit,會使得未來需要作 merge 的時候遇到許多麻煩

例如常用的 repository 結構如下:

/trunk/
/branches/helloworld-1.0/
/branches/helloworld-1.1/

要防止使用者同時對兩個以上的 branch (包含 main trunk) 作 commit,
可以把下面這一段程式再加進去 pre-commit 的 hook 中

[Subversion pre-commit hook to reject cross-branch commit]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/perl
 
# pre-commit hook to reject empty commit log and cross-branch commit
# remember to chmod 755 on this file
 
die "Usage: $0 [REPOS] [TXN]\n" unless @ARGV > 1;
 
$REPOS=$ARGV[0];
$TXN=$ARGV[1];
 
$svnlook = '/usr/local/bin/svnlook';
 
chomp($author=`$svnlook author -t $TXN $REPOS`);
chomp($log=`$svnlook log -t $TXN $REPOS`);
 
if( $log eq '' || $log =~ /^\W+$/ ) {
    die "\nHello, $author. Empty commit log is not permitted!\n";
}
 
open(DIRS,"$svnlook dirs-changed -t $TXN $REPOS|");
while(<DIRS>) {
    if( /^(trunk)\// ) {
        $branches{$1}++;
    }
    elsif( /^branches\/([^\/]+)/ ) {
         $branches{$1}++;
    }
}
close(DIRS);
 
$c = %branches;
 
if( $c > 1 ) {
    die "\nHello, $author. You can't commit to $c branches at the same time!\n";
}
 
exit(0);
 Posted by at 00:54
一月 022008
 

SSH client 有一個 Escape Character (跳脫字元),事實上 rsh/rlogin 也有支援 Escape Character (~),就如同 Ctrl-] 之於 telnet 一樣…

SSH client 的 Escape Character 一般跟 rsh/rlogin 一樣設定成 ~,詳細用法可於命令列按 ~? 來取得說明,要注意的是,為了避免影響一般正常的輸入,Escape Character 必須是命令列換行後的第一個按鍵,如果你已經輸入別的按鍵,即使按 backspace 把游標移回開頭處再輸入 Escape Character 也是無效的!請先按 Enter 換行後再輸入吧!

myhost# ~?
Supported escape sequences:
~.  - terminate connection
~B  - send a BREAK to the remote system
~C  - open a command line
~R  - Request rekey (SSH protocol 2 only)
~^Z - suspend ssh
~#  - list forwarded connections
~&  - background ssh (when waiting for connections to terminate)
~?  - this message
~~  - send the escape character by typing it twice
(Note that escapes are only recognized immediately after newline.)

最常用的 Escape Character 就是 ~. 以及 ~^Z 這兩個:

~. 是直接切斷目前的 SSH 連線,如果你遇到遠端機器沒有反應時,可用此方式來切斷連線

~^Z (~ 及 Ctrl-Z) 則是把目前的 SSH 連線先 suspend,一般可用來回到原來機器上處理一些事情,若要再回去,輸入 fg 即可

問題來了,假設你從 HOST1 先 SSH 到 HOST2,然後再從 HOST2 SSH 到 HOST3

HOST1 ===1===> HOST2 ===2===> HOST3

如果你在 HOST3 的 terminal 下輸入 ~. 的話,你會直接切斷上面的第一段連線回到 HOST1 的 terminal,當然這樣的話第二段連線也一併 bye-bye 了!如果只是想切斷第二段連線回到 HOST2 該怎麼作呢?

這時候 ~~ 就發揮作用了,你只要輸入 ~~. 就可以切斷第二段連線了

以此累推,輸入 N 個 Escape Character 代表對第 N 段連線送出 Escape Character

最後,若不想用 ~ 當作跳脫字元,可於命令列用 -e 修改,例如把 SSH 跳脫字元改成 #:

ssh -e "#" myhost.mydomain.com
 Posted by at 21:36
十二月 242007
 

這是今年1月寫的 script… 用來監控各大 FreeBSD CVSUP mirror 網站的檔案更新程度,
提供服務的網址是 http://ftp.giga.net.tw/cvsup.php

內容其實很簡單,下面把程式放出來,有興趣的人歡迎免費拿去修改使用

==
原理說明:

CVS 不像 Subversion 一樣可以很容易知道目前 repository 中的最新版本為何,
必須要把每個檔案檢查一遍才有辦法知道最新的版本是什麼…

不過在 FreeBSD Source CVS 下面,每次 commit 都會有 commit log 可經由 CVSUP 取得,
因此我們可以利用 CVSROOT-*/commitlogs/* 這幾個檔案來判斷目前 source tree 的狀態..
只要把這幾個檔案經由 CVSUP 取出來就可以大致知道該伺服器的目前保存的最新版本是哪一個了
==

第一個是一個 Perl script,主要是連結到各大 FreeBSD CVSUP 網站取得相關資訊存下來
後面則附上一個 PHP script,可以把這些資訊用 HTML 表格方式呈現出來

check-cvsup.pl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#!/usr/bin/perl
 
# Check the update status for CVSUP servers
# BSD license
# Shen Cheng-Da (cdsheen AT gmail.com)
 
use POSIX qw(strftime);
use Net::DNS;
use Time::HiRes qw( gettimeofday tv_interval );
 
@servers = qw( cvsup.tw.freebsd.org cvsup1.tw.freebsd.org cvsup2.tw.freebsd.org
                cvsup3.tw.freebsd.org cvsup4.tw.freebsd.org cvsup5.tw.freebsd.org
                cvsup6.tw.freebsd.org cvsup7.tw.freebsd.org cvsup8.tw.freebsd.org
                cvsup9.tw.freebsd.org cvsup10.tw.freebsd.org cvsup11.tw.freebsd.org
                cvsup12.tw.freebsd.org cvsup13.tw.freebsd.org cvsup14.tw.freebsd.org );
 
$dir    = '/home/cvsup-monitor';
 
$cvsup  = '/usr/local/bin/cvsup';
$base   = $dir . '/base';
$log    = $dir . '/check.log';
 
chdir( $dir ) || die "ERROR: Can not change to directory [$dir]\n";
 
mkdir( $base, 0755 ) unless -d $base;
 
@files_src = qw( CVSROOT-src/commitlogs/CVSROOT CVSROOT-src/commitlogs/bin
                CVSROOT-src/commitlogs/etc CVSROOT-src/commitlogs/contrib
                CVSROOT-src/commitlogs/gnu CVSROOT-src/commitlogs/games
                CVSROOT-src/commitlogs/include CVSROOT-src/commitlogs/lib
                CVSROOT-src/commitlogs/release CVSROOT-src/commitlogs/sys
                CVSROOT-src/commitlogs/sbin CVSROOT-src/commitlogs/share
                CVSROOT-src/commitlogs/tools CVSROOT-src/commitlogs/user
                CVSROOT-src/commitlogs/usrbin CVSROOT-src/commitlogs/usrsbin );
 
@files_ports = qw( CVSROOT-ports/commitlogs/CVSROOT
                        CVSROOT-ports/commitlogs/ports );
 
$includes = '';
foreach $f ( @files_src ) {
        $includes .= " -i $f";
}
foreach $f ( @files_ports ) {
        $includes .= " -i $f";
}
 
open( LOG, ">$log");
 
my $resolver = Net::DNS::Resolver->new;
 
$resolver->usevc(1);
$resolver->udp_timeout(5);
$resolver->tcp_timeout(5);
$resolver->retrans(3);
$resolver->retry(2);
$resolver->persistent_tcp(1);
 
foreach $server ( @servers ) {
        my $query = $resolver->query($server, 'A');
        @ipaddr = @cname = ();
        if( $query ) {
                foreach $rr ($query->answer) {
                        if( $rr->type eq 'A' ) {
                                print LOG "$server => [A] ".$rr->address."\n";
                                push( @ipaddr, $rr->address );
                        }
                        elsif( $rr->type eq 'CNAME' ) {
                                print LOG "$server => [CNAME] ".$rr->cname."\n";
                                push( @cname, $rr->cname );
                        }
                }
        }
        $ip = $ipaddr[0];
        $ipaddr = join( '|', @ipaddr );
        $cname  = join( '|', @cname  );
 
        $ibase   = "$base/$server";
        mkdir( $ibase, 0755 ) unless -d $ibase;
        mkdir( "$ibase/SRC", 0755 ) unless -d "$ibase/SRC";
 
        $cmd = "$cvsup -h $ip -L 0 -b $ibase -r 0 $includes supfile";
 
        $time_start = [gettimeofday];
        system($cmd);
        $elapsed = tv_interval( $time_start );
 
        printf LOG ("$server => cvsup on $ip elapsed %.2fs\n", $time_elapsed);
 
        unless( $? ) {
                $mt_src = $mt_ports = 0;
                foreach $f ( @files_src ) {
                        $t = (stat( "$ibase/SRC/$f" ))[9];
                        $mt_src = $t if $t > $mt_src;
                }
                foreach $f ( @files_ports ) {
                        $t = (stat( "$ibase/SRC/$f" ))[9];
                        $mt_ports = $t if $t > $mt_ports;
                }
                open( REC, ">$ibase/last-commit.txt" );
                printf REC("$mt_src,$mt_ports,%.2f,$ipaddr,$cname",$elapsed);
                close(REC);
 
                printf LOG ("$server => SRC: %s\n",
                        strftime("%Y/%m/%d %H:%M:%S", localtime($mt_src)) );
                printf LOG ("$server => PORTS: %s\n",
                        strftime("%Y/%m/%d %H:%M:%S", localtime($mt_ports)) );
        }
}
 
close(LOG);

supfile:

*default prefix=SRC
*default release=cvs delete use-rel-suffix
*default compress
 
cvsroot-src
cvsroot-ports

cvsup.php

<style type="text/css">
  td       { font-size: 12px; vertical-align: top }
  td.head  { background: #FFFFC0 }
  td.c     { background: #E0E0FF }
  td.red   { background: #FFE0E0 }
  td.green { background: #E0FFE0 }
</style>
<table border=1 cellspacing=0 cellpadding=2>
<tr><td class=head>Server Name</td><td class=head>IP</td>
<td class=head>CNAME</td><td class=head>latest commit of src</td>
<td class=head>latest commit of ports</td>
</tr>
<?
        $dir = '/home/cvsup-monitor';
        $servers = array(
              'cvsup.tw.freebsd.org',  'cvsup1.tw.freebsd.org',  'cvsup2.tw.freebsd.org',
              'cvsup3.tw.freebsd.org', 'cvsup4.tw.freebsd.org', 'cvsup5.tw.freebsd.org',
              'cvsup6.tw.freebsd.org', 'cvsup7.tw.freebsd.org', 'cvsup8.tw.freebsd.org',
              'cvsup9.tw.freebsd.org', 'cvsup10.tw.freebsd.org', 'cvsup11.tw.freebsd.org',
              'cvsup12.tw.freebsd.org', 'cvsup13.tw.freebsd.org', 'cvsup14.tw.freebsd.org' );
        $check = time() - 86400;
        $time_format = '%Y/%m/%d %H:%M:%S';
        $latest_src = $latest_ports = 0;
        foreach( $servers as $server ) {
                $data = file_get_contents("$dir/base/$server/last-commit.txt");
                $data = trim($data);
                if( $data != '' ) {
                        $SERVER[$server] = explode(',', $data);
                        if( $SERVER[$server][0] > $latest_src )
                                $latest_src = $SERVER[$server][0];
                        if( $SERVER[$server][1] > $latest_ports )
                                $latest_ports = $SERVER[$server][1];
                }
        }
        foreach( $servers as $server ) {
                if( is_array($SERVER[$server]) ) {
                        list( $src, $ports, $elapsed, $ipaddr, $aliases ) = $SERVER[$server];
                        $ipaddr = str_replace( '|', '<br />', $ipaddr );
                        $aliases = str_replace( '|', '<br />', $aliases );
                        if( $aliases == '' )
                                $aliases = '&nbsp;';
                        print "<tr>\n";
                        print "  <td class=c>$server</td>\n";
                        print "  <td class=c>$ipaddr</td>\n";
                        print "  <td class=c>$aliases</td>\n";
                        if( $src == $latest_src )
                                print "  <td class=green>";
                        elseif( $src < $check )
                                print "  <td class=red>";
                        else
                                print "  <td class=c>";
                        print strftime( $time_format, $src ) . "</td>\n";
                        if( $ports == $latest_ports )
                                print "  <td class=green>";
                        elseif( $ports < $check )
                                print "  <td class=red>";
                        else
                                print "  <td class=c>";
                        print strftime( $time_format, $ports ) . "</td>\n";
#                       print "  <td align=right>$elapsed s</td>\n";
                        print "</tr>\n";
                }
        }
?>
</table>
 Posted by at 23:52