面倒だったiperf3のリアルタイム表示+EXE化
Windowsのiperf3.exeの実行結果をリアルタイムに表示する、そのプログラムをEXE化するのに苦労した話(表示結果の一部をCSVファイル化することも実施)。相変わらず、美しくないコード部分は存在する。
wexpect利用(Subprocess利用はだめだった)
「ろうとるがPythonを扱う、、(その9:まとも版コマンドプロンプトもどき改良版)」にて、実行結果をリアルタイムに表示するときに用いた、Subprocessを利用しようとしたが、リアルタイム表示できず、、。調べると次の情報が見つかる。
この中で、下記記述が見つかる。
- 3.1.5 以降の新しい iperf3.exe を入手して、 --forceflush オプションつきで実行して下さい。
- iperf3.exe (3.1.3) であれば 各テスト後の flush が実行されるまで出力が受け取れないことになり
しかし、Windows版のiperf3の最新版(バイナリ)はv3.1.3である。同記事を見ていくと、
- 例えば Python で wexpect という (本来は、コンソールに対して対話的なキー入力操作を求めるプログラムを自動操作するための) モジュールを利用する
というわけで、wexpectを利用することとした。
結果(実行の様子)
Clientの結果例である。
Serverの結果例である。
Configuration(パラメーター設定)の様子。
ソースコード
いつものように、ポイントを記載。
import
import wexpect
import threading
import logging
import logging.handlers
import time
from datetime import datetime
from tkinter import *
import tkinter.ttk as ttk
from socket import inet_aton
import re
ここはノーコメント。
定義
# List
tab_list = [' Client ', ' Server ', ' Configuration ']
btn_list = [' Start ', ' Stop ']
log_list = ['iperf-client', 'iperf-server']
# Flag
Doing = [0, 0] # Client, Server
# Parameters, etc
default_port = 5201
default_length = 512
spawn_timeout = 300
# Unit at Report
unit = ['Kbits', 'Mbits', 'Non-Specified']
prm_unit = ['k', 'm']
- 各種リスト(タブ、ボタン、ログ)
- 動作中かどうかのフラグ
- デフォルト値(ポート番号、パケット長、プロセスの待ち時間)
- 結果の単位
メインプログラム
############### Start of main program ###############
# root main window
root = Tk()
root.title("iperf v0.93")
root.geometry("700x400")
# Create an instance of ttk style
fgcolor = "lightskyblue2"
bgcolor = "gray80"
style = ttk.Style()
style.theme_create("style1", parent="alt", settings={
"TNotebook.Tab": {
"configure": {"background": bgcolor },
"map": {"background": [("selected", fgcolor)],
} } } )
style.theme_use("style1")
# Create Notebook Widget
note = ttk.Notebook(root)
# Create tab
tab0 = Frame(note) # Client
tab1 = Frame(note) # Server
tab2 = Frame(note) # Configuration
# Add tab
note.add(tab0, text=tab_list[0])
note.add(tab1, text=tab_list[1])
note.add(tab2, text=tab_list[2])
# Create content of tab
create_content(tab0, 0) # Client
create_content(tab1, 1) # Server
create_config(tab2) # Configuration
# Locate tab
note.pack(expand=True, fill='both')
# Log
log = [logging.getLogger(log_list[i]) for i in range(len(log_list))]
[log[i].setLevel(logging.DEBUG) for i in range(len(log_list))]
fmt = '{:-%Y%m%d-%H%M%S}.csv'.format(datetime.now())
log_file = [(log_list[i] + fmt) for i in range(len(log_list))]
fh = [logging.FileHandler(log_file[i]) for i in range(len(log_list))]
[log[i].addHandler(fh[i]) for i in range(len(log_list))]
# main loop
root.mainloop()
- Window定義
- タブの色付け設定(style)
- Client及びServerタブ作成(create_content)
- 設定タブ作成(create_config)
- ログ設定
詳細は、小生の過去の投稿を参考。
iperf3 Client及びServer用タブ
### Client&Server Tab (create_content) ###
def create_content(frm, arg):
frm1 = Frame(frm)
frm2 = Frame(frm)
frm1.pack()
frm2.pack()
# Frame 1
if arg%2 == 0: # Client
label1 = Label(frm1, text=' IP address ')
label1.grid(row=0, column=0)
frm.entryIP = Entry(frm1, width=15)
frm.entryIP.grid(row=0, column=1)
label2 = Label(frm1, text=' Bandwith ')
label2.grid(row=0, column=2)
frm.entryBandwidth = Entry(frm1, width=15)
frm.entryBandwidth.grid(row=0, column=3)
label3 = Label(frm1, text=' Time(sec) ')
label3.grid(row=0, column=4)
frm.entrySecond = Entry(frm1, width=15)
frm.entrySecond.grid(row=0, column=5)
frm.Btn = Button(frm1, text=btn_list[0], command=lambda:btn_clk(frm, arg))
frm.Btn.grid(row=0, column=6)
else: # Server
frm.Btn = Button(frm1, text=btn_list[0], command=lambda:btn_clk(frm, arg))
frm.Btn.grid(row=0, column=0)
# Frame 2
frm.text = Text(frm2, width=100, height=40)
frm.ysc = Scrollbar(frm2, orient=VERTICAL, command=frm.text.yview)
frm.text["yscrollcommand"] = frm.ysc.set
frm.ysc.pack(side=RIGHT, fill="y")
frm.text.config(state='disabled')
frm.text.pack()
return
上述した結果(表示)参照。
- Client:IPアドレス、帯域、実行時間(回数)、実行ボタン
- Server:実行ボタン
- 結果表示領域の定義(スクロールバーつき)
パラメーター設定タブ
### Configuration Tab (create_config) ###
def create_config(frm):
# Parameter handling
global select_port
global select_length
global select_tcp
global select_unit
select_port = default_port
select_length = default_length
select_tcp = BooleanVar(value = False) # UDP
select_unit = IntVar(value = 2) # Not specified unit at format
# Locate items
label1 = Label(frm, text='Port Number')
label1.place(x=10, y=10)
frm.port = Entry(frm, width=6)
frm.port.insert(0, select_port)
frm.port.place(x=150, y=10)
label2 = Label(frm, text='Length')
label2.place(x=10, y=40)
frm.length = Entry(frm, width=6)
frm.length.insert(0, select_length)
frm.length.place(x=150, y=40)
frm.tcp = Checkbutton(frm, text=' Use of TCP', variable=select_tcp)
frm.tcp.place(x=10, y=70)
label3 = Label(frm, text='Unit at report')
label3.place(x=10, y=100)
frm.unit = [Radiobutton(frm, text=unit[i], variable=select_unit, value=i) for i in range(len(unit))]
[frm.unit[i].place(x=150+i*70, y=100) for i in range(len(unit))]
frm.btn = Button(frm, text=" Save ", command=lambda:save_btn_clk(frm))
frm.btn.place(x=150, y=130)
return
ここも上述した結果(表示)参照。
- ポート番号入力ボックス(デフォルト値入力)
- パケット長入力ボックス(デフォルト値入力)
- TCP選択チェックボックス(デフォルトはUDP)
- 結果出力時の単位選択ラジオボタン(Kbps,Mbps,指定なし)
パラメータ設定ボタンクリック時処理
### Save Configuration Parameters (save_btn_clk) ###
def save_btn_clk(frm):
global select_port
global select_length
global select_tcp
global select_unit
try:
select_port = int(frm.port.get())
except:
select_port = default_port
frm.port.delete(0, END)
frm.port.insert(0, select_port)
try:
select_length = int(frm.length.get())
except:
select_length = default_length
frm.length.delete(0, END)
frm.length.insert(0, select_length)
select_tcp.set(select_tcp.get())
select_unit.set(select_unit.get())
return
- ポート番号及びパケット長の入力値を取得(エラー時はデフォルト値を設定)
- TCP選択チェックボックスの状態取得
- 結果出力時の単位取得
iperf ClientおよびServer実行ボタンクリック時処理
### Start iperf (bth_clk) ###
def btn_clk(frm, arg):
if arg%2 == 0: # Client
ip = frm.entryIP.get()
if is_valid_ip(ip) == False:
ip = '127.0.0.1' # Loopback
bw = frm.entryBandwidth.get()
if is_valid_bandwidth(bw) == False:
bw = '' # Not specified
try:
sec = int(frm.entrySecond.get())
except:
sec = 10 # 10 times
else: # Server
# all are dummy
ip = ''
bw = ''
sec = 0
ExecIperf(frm, arg, ip, bw, sec)
return
- Client
- 入力されたIPアドレス、帯域、実行時間(回数)を取得
- 入力エラー時にはコードで示された値を代入など
- Server
- すべてダミーの値を設定
- iperf実行関数呼び出し
入力値のエラーチェック
### Check IP Address Format (is_valid_ip) ###
def is_valid_ip(addr):
try:
inet_aton(addr)
return True
except:
return False
### Bandwidth Parameter Check (is_valid_bandwidth) ###
def is_valid_bandwidth(arg):
if arg.count('.') <= 1: # Only 1 decimal point
m = re.match(r'[0-9][0-9.]*[kKmMgG]', arg)
if m == None:
return False
else:
return True
else:
return False
- IPアドレス形式のチェック(inet_aton()利用)
- 帯域値のチェック(数値に小数点は1つ、単位)
スレッド起動
### Start Thread (ExecIperf) ###
def ExecIperf(frm, arg, ip, bw, sec):
global proc
if Doing[arg%2] == 0:
Doing[arg%2] = 1
th = threading.Thread(target=IperfThread, args=(frm, arg, ip, bw, sec), daemon=True)
th.start()
else:
Doing[arg%2] = 0
if 'proc' in globals():
proc.terminate()
out = 'Stop Clicked'
log[arg].debug(out)
frm.text.insert(END, out)
frm.text.see('end')
frm.Btn['text'] = btn_list[Doing[arg%2]]
return
- Startクリック時には、フラグ(Doing)をON(1)にして、スレッド呼び出し、ボタンをStopに変更
- Stopクリック時には、フラグ(Doing)をOFF(0)にして、スレッド実行中であれば、スレッドを停止し、画面表示およびログにStopを記録、ボタンをStartに変更
スレッド
### Main Thread (IperfThread) ###
def IperfThread(frm, arg, ip, bw, sec):
global select_port
global select_length
global select_tcp
global proc
global start_flag
if arg%2 == 0: # Client
cmd = 'iperf3.exe -c ' + ip
if select_tcp.get() == False:
cmd += ' -u'
cmd += ' -l ' + str(select_length)
if bw != '':
cmd += ' -b ' + bw
cmd += ' -t ' + str(sec)
else: # Server
cmd = 'iperf3.exe -s'
if select_port != default_port:
cmd += ' -p ' + str(select_port)
if select_unit.get() < 2:
cmd += ' -f ' + prm_unit[select_unit.get()]
frm.text.config(state='normal')
frm.text.insert(END, '\n')
proc = wexpect.spawn(cmd, timeout=spawn_timeout) # Call iperf3
start_flag = 0
try:
for line in iter(proc.readline, ''):
if line != 0:
out = line.rstrip()
if arg%2 == 0: # Client
log[arg].debug(out) # Not CSV
else: # Server
#log[arg].debug(out)
ToCSV(arg, out)
frm.text.insert(END, out + '\n')
frm.text.see('end')
if Doing[arg%2] == 0:
break
except:
out = "Timeout happens. Click 'Start' again."
log[arg].debug(out)
frm.text.insert(END, out)
frm.text.see('end')
frm.text.config(state='disabled')
Doing[arg%2] = 0
frm.Btn['text'] = btn_list[Doing[arg%2]]
return
- 入力値や設定パラメーターをセットして、iperf3をwexpect.spawnにて実行
- 実行結果1行(proc.readline)ずつログに記録し、画面表示
- 終了時には、フラグ(Doing)をOFF(0)にして、ボタンをStartに変更
CSVファイル化
### CSV File (ToCSV) ###
def ToCSV(arg, line):
global start_flag
global select_tcp
m = re.search(r'\[.+\]*', line) # OK beginning with [ ]
if m != None:
if re.search('local', line) == None:
if re.search('ID', line):
if start_flag == 0:
if select_tcp.get() == False:
ID_list = 'Interval, , Transfer, , Bandwidth, , Jitter, , Lost/Total Datagrams, Percent,'
else:
ID_list = 'Interval, , Transfer, , Bandwidth, ,'
log[arg].debug(ID_list)
start_flag = 1
else:
start_flag = 0
else:
if start_flag == 1:
data_list = line.split(' ')
csv = ''
for item in data_list:
if len(item) > 0 and re.search(r'\[', item) == None and re.search(r'\]', item) == None:
csv += item + ', '
log[arg].debug(csv)
return
Server側の実行結果は下記となる。
-----------------------------------------------------------
Server listening on 5201
-----------------------------------------------------------
Accepted connection from 127.0.0.1, port 1043
[ 5] local 127.0.0.1 port 5201 connected to 127.0.0.1 port 50917
[ ID] Interval Transfer Bandwidth Jitter Lost/Total Datagrams
[ 5] 0.00-1.01 sec 340 KBytes 2747 Kbits/sec 0.084 ms 0/679 (0%)
[ 5] 1.01-2.01 sec 374 KBytes 3083 Kbits/sec 0.137 ms 0/749 (0%)
...
[ 5] 19.00-20.01 sec 366 KBytes 2950 Kbits/sec 0.086 ms 0/731 (0%)
[ 5] 20.01-20.02 sec 512 Bytes 4537 Kbits/sec 0.096 ms 0/1 (0%)
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bandwidth Jitter Lost/Total Datagrams
[ 5] 0.00-20.02 sec 0.00 Bytes 0.00 Kbits/sec 0.096 ms 0/14597 (0%)
- [ ID]で始まる最初の行をCSVファイルのヘッダ行とする
- 次の[ ID]で始まる行までをデータとしてCSV化する
全体
# -*- coding: utf-8 -*-
import wexpect
import threading
import logging
import logging.handlers
import time
from datetime import datetime
from tkinter import *
import tkinter.ttk as ttk
from socket import inet_aton
import re
# List
tab_list = [' Client ', ' Server ', ' Configuration ']
btn_list = [' Start ', ' Stop ']
log_list = ['iperf-client', 'iperf-server']
# Flag
Doing = [0, 0] # Client, Server
# Parameters, etc
default_port = 5201
default_length = 512
spawn_timeout = 300
# Unit at Report
unit = ['Kbits', 'Mbits', 'Non-Specified']
prm_unit = ['k', 'm']
### CSV File (ToCSV) ###
def ToCSV(arg, line):
global start_flag
global select_tcp
m = re.search(r'\[.+\]*', line) # OK beginning with [ ]
if m != None:
if re.search('local', line) == None:
if re.search('ID', line):
if start_flag == 0:
if select_tcp.get() == False:
ID_list = 'Interval, , Transfer, , Bandwidth, , Jitter, , Lost/Total Datagrams, Percent,'
else:
ID_list = 'Interval, , Transfer, , Bandwidth, ,'
log[arg].debug(ID_list)
start_flag = 1
else:
start_flag = 0
else:
if start_flag == 1:
data_list = line.split(' ')
csv = ''
for item in data_list:
if len(item) > 0 and re.search(r'\[', item) == None and re.search(r'\]', item) == None:
csv += item + ', '
log[arg].debug(csv)
return
### Main Thread (IperfThread) ###
def IperfThread(frm, arg, ip, bw, sec):
global select_port
global select_length
global select_tcp
global proc
global start_flag
if arg%2 == 0: # Client
cmd = 'iperf3.exe -c ' + ip
if select_tcp.get() == False:
cmd += ' -u'
cmd += ' -l ' + str(select_length)
if bw != '':
cmd += ' -b ' + bw
cmd += ' -t ' + str(sec)
else: # Server
cmd = 'iperf3.exe -s'
if select_port != default_port:
cmd += ' -p ' + str(select_port)
if select_unit.get() < 2:
cmd += ' -f ' + prm_unit[select_unit.get()]
frm.text.config(state='normal')
frm.text.insert(END, '\n')
proc = wexpect.spawn(cmd, timeout=spawn_timeout) # Call iperf3
start_flag = 0
try:
for line in iter(proc.readline, ''):
if line != 0:
out = line.rstrip()
if arg%2 == 0: # Client
log[arg].debug(out) # Not CSV
else: # Server
#log[arg].debug(out)
ToCSV(arg, out)
frm.text.insert(END, out + '\n')
frm.text.see('end')
if Doing[arg%2] == 0:
break
except:
out = "Timeout happens. Click 'Start' again."
log[arg].debug(out)
frm.text.insert(END, out)
frm.text.see('end')
frm.text.config(state='disabled')
Doing[arg%2] = 0
frm.Btn['text'] = btn_list[Doing[arg%2]]
return
### Start Thread (ExecIperf) ###
def ExecIperf(frm, arg, ip, bw, sec):
global proc
if Doing[arg%2] == 0:
Doing[arg%2] = 1
th = threading.Thread(target=IperfThread, args=(frm, arg, ip, bw, sec), daemon=True)
th.start()
else:
Doing[arg%2] = 0
if 'proc' in globals():
proc.terminate()
out = 'Stop Clicked'
log[arg].debug(out)
frm.text.insert(END, out)
frm.text.see('end')
frm.Btn['text'] = btn_list[Doing[arg%2]]
return
### Check IP Address Format (is_valid_ip) ###
def is_valid_ip(addr):
try:
inet_aton(addr)
return True
except:
return False
### Bandwidth Parameter Check (is_valid_bandwidth) ###
def is_valid_bandwidth(arg):
if arg.count('.') <= 1: # Only 1 decimal point
m = re.match(r'[0-9][0-9.]*[kKmMgG]', arg)
if m == None:
return False
else:
return True
else:
return False
### Start iperf (bth_clk) ###
def btn_clk(frm, arg):
if arg%2 == 0: # Client
ip = frm.entryIP.get()
if is_valid_ip(ip) == False:
ip = '127.0.0.1' # Loopback
bw = frm.entryBandwidth.get()
if is_valid_bandwidth(bw) == False:
bw = '' # Not specified
try:
sec = int(frm.entrySecond.get())
except:
sec = 10 # 10 times
else: # Server
# all are dummy
ip = ''
bw = ''
sec = 0
ExecIperf(frm, arg, ip, bw, sec)
return
### Client&Server Tab (create_content) ###
def create_content(frm, arg):
frm1 = Frame(frm)
frm2 = Frame(frm)
frm1.pack()
frm2.pack()
# Frame 1
if arg%2 == 0: # Client
label1 = Label(frm1, text=' IP address ')
label1.grid(row=0, column=0)
frm.entryIP = Entry(frm1, width=15)
frm.entryIP.grid(row=0, column=1)
label2 = Label(frm1, text=' Bandwith ')
label2.grid(row=0, column=2)
frm.entryBandwidth = Entry(frm1, width=15)
frm.entryBandwidth.grid(row=0, column=3)
label3 = Label(frm1, text=' Time(sec) ')
label3.grid(row=0, column=4)
frm.entrySecond = Entry(frm1, width=15)
frm.entrySecond.grid(row=0, column=5)
frm.Btn = Button(frm1, text=btn_list[0], command=lambda:btn_clk(frm, arg))
frm.Btn.grid(row=0, column=6)
else: # Server
frm.Btn = Button(frm1, text=btn_list[0], command=lambda:btn_clk(frm, arg))
frm.Btn.grid(row=0, column=0)
# Frame 2
frm.text = Text(frm2, width=100, height=40)
frm.ysc = Scrollbar(frm2, orient=VERTICAL, command=frm.text.yview)
frm.text["yscrollcommand"] = frm.ysc.set
frm.ysc.pack(side=RIGHT, fill="y")
frm.text.config(state='disabled')
frm.text.pack()
return
### Save Configuration Parameters (save_btn_clk) ###
def save_btn_clk(frm):
global select_port
global select_length
global select_tcp
global select_unit
try:
select_port = int(frm.port.get())
except:
select_port = default_port
frm.port.delete(0, END)
frm.port.insert(0, select_port)
try:
select_length = int(frm.length.get())
except:
select_length = default_length
frm.length.delete(0, END)
frm.length.insert(0, select_length)
select_tcp.set(select_tcp.get())
select_unit.set(select_unit.get())
return
### Configuration Tab (create_config) ###
def create_config(frm):
# Parameter handling
global select_port
global select_length
global select_tcp
global select_unit
select_port = default_port
select_length = default_length
select_tcp = BooleanVar(value = False) # UDP
select_unit = IntVar(value = 2) # Not specified unit at format
# Locate items
label1 = Label(frm, text='Port Number')
label1.place(x=10, y=10)
frm.port = Entry(frm, width=6)
frm.port.insert(0, select_port)
frm.port.place(x=150, y=10)
label2 = Label(frm, text='Length')
label2.place(x=10, y=40)
frm.length = Entry(frm, width=6)
frm.length.insert(0, select_length)
frm.length.place(x=150, y=40)
frm.tcp = Checkbutton(frm, text=' Use of TCP', variable=select_tcp)
frm.tcp.place(x=10, y=70)
label3 = Label(frm, text='Unit at report')
label3.place(x=10, y=100)
frm.unit = [Radiobutton(frm, text=unit[i], variable=select_unit, value=i) for i in range(len(unit))]
[frm.unit[i].place(x=150+i*70, y=100) for i in range(len(unit))]
frm.btn = Button(frm, text=" Save ", command=lambda:save_btn_clk(frm))
frm.btn.place(x=150, y=130)
return
############### Start of main program ###############
# root main window
root = Tk()
root.title("iperf v0.93")
root.geometry("700x400")
# Create an instance of ttk style
fgcolor = "lightskyblue2"
bgcolor = "gray80"
style = ttk.Style()
style.theme_create("style1", parent="alt", settings={
"TNotebook.Tab": {
"configure": {"background": bgcolor },
"map": {"background": [("selected", fgcolor)],
} } } )
style.theme_use("style1")
# Create Notebook Widget
note = ttk.Notebook(root)
# Create tab
tab0 = Frame(note) # Client
tab1 = Frame(note) # Server
tab2 = Frame(note) # Configuration
# Add tab
note.add(tab0, text=tab_list[0])
note.add(tab1, text=tab_list[1])
note.add(tab2, text=tab_list[2])
# Create content of tab
create_content(tab0, 0) # Client
create_content(tab1, 1) # Server
create_config(tab2) # Configuration
# Locate tab
note.pack(expand=True, fill='both')
# Log
log = [logging.getLogger(log_list[i]) for i in range(len(log_list))]
[log[i].setLevel(logging.DEBUG) for i in range(len(log_list))]
fmt = '{:-%Y%m%d-%H%M%S}.csv'.format(datetime.now())
log_file = [(log_list[i] + fmt) for i in range(len(log_list))]
fh = [logging.FileHandler(log_file[i]) for i in range(len(log_list))]
[log[i].addHandler(fh[i]) for i in range(len(log_list))]
# main loop
root.mainloop()
EXE化
pyinstallerを用いて、EXE化するが、機能しない(iperf3.exeがCallされない)状況が発生。調べると、下記サイトが見つかる。
後者の記載内容を実施することで、EXE化して実行することことができた。
- 後者サイトからダウンロードしたwexpect-master.zipを展開し、展開したフォルダーに移動
- ”pyinstaller wexpect.spec”を実行
- そのフォルダーに、上記ソースコード(”iperf_tkiniter.py”というコード名とする)をコピー
- ”pyinstaller iperf_tkiniter.py”を実行
- ”dist\iperf_tkiniter.py\iperf_tkiniter.exe”が完成
- ”wexpect”と”iperf_tkiniter”を含んだdistを配布(利用)対象とする
それでも、"--onefile"(ファイルを1つにまとめる)は不可であった。
EOF