ろうとるがPythonを扱う、、（その15：iperf3をtkinterでリアルタイム表示+EXE化）

# 面倒だったiperf3のリアルタイム表示＋EXE化
Windowsのiperf3.exeの実行結果をリアルタイムに表示する、そのプログラムをEXE化するのに苦労した話（表示結果の一部をCSVファイル化することも実施）。相変わらず、美しくないコード部分は存在する。

## wexpect利用(Subprocess利用はだめだった)
「[ろうとるがPythonを扱う、、（その９：まとも版コマンドプロンプトもどき改良版）](https://qiita.com/infinite1oop/items/7898f091c87837704f8f)」にて、実行結果をリアルタイムに表示するときに用いた、Subprocessを利用しようとしたが、リアルタイム表示できず、、。調べると次の情報が見つかる。
- [PowerShell上でのexe実行時の文字列追加毎のタイミングでにタイムスタンプをつけたい](https://teratail.com/questions/310113)

この中で、下記記述が見つかる。
- 3.1.5 以降の新しい iperf3.exe を入手して、 --forceflush オプションつきで実行して下さい。
-  iperf3.exe (3.1.3) であれば 各テスト後の flush が実行されるまで出力が受け取れないことになり

しかし、Windows版のiperf3の最新版（バイナリ）はv3.1.3である。同記事を見ていくと、
- 例えば Python で wexpect という (本来は、コンソールに対して対話的なキー入力操作を求めるプログラムを自動操作するための) モジュールを利用する

というわけで、wexpectを利用することとした。

## 結果（実行の様子）
Clientの結果例である。
![Client.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/77a03764-330f-ff86-dc87-63961dde63a0.png)
Serverの結果例である。
![Server.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/0876a3d9-3442-09b6-0612-e33b1b33c4f2.png)
Configuration（パラメーター設定）の様子。
![Configuration.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/2c01ebcb-79a2-7745-f288-9f971c93c2d9.png)


## ソースコード
いつものように、ポイントを記載。

### import
```python
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
```
ここはノーコメント。

### 定義
```python
# 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']
```
- 各種リスト（タブ、ボタン、ログ）
- 動作中かどうかのフラグ
- デフォルト値（ポート番号、パケット長、プロセスの待ち時間）
- 結果の単位

### メインプログラム
```python
############### 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用タブ
``` python
### 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：実行ボタン
- 結果表示領域の定義（スクロールバーつき）

### パラメーター設定タブ
``` python
### 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,指定なし）

### パラメータ設定ボタンクリック時処理
``` python
### 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実行ボタンクリック時処理
``` python
### 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実行関数呼び出し

### 入力値のエラーチェック
``` python
### 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()利用）
- 帯域値のチェック（数値に小数点は１つ、単位）

### スレッド起動
```python
### 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に変更

### スレッド
``` python
### 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にて実行
- 実行結果１行（proc.readline）ずつログに記録し、画面表示
- 終了時には、フラグ（Doing）をOFF(0)にして、ボタンをStartに変更

### CSVファイル化
```python
### 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化する

### 全体
```python
# -*- 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されない）状況が発生。調べると、下記サイトが見つかる。
- [run an executable using wexpect](https://stackoverflow.com/questions/11320639/run-an-executable-using-wexpect)
- [How to use Wexpect with pyinstaller](https://github.com/raczben/wexpect/wiki/Wexpect-with-pyinstaller#how-to-use-wexpect-with-pyinstaller)

後者の記載内容を実施することで、EXE化して実行することことができた。
1. 後者サイトからダウンロードしたwexpect-master.zipを展開し、展開したフォルダーに移動
1. ”pyinstaller wexpect.spec”を実行
1. そのフォルダーに、上記ソースコード（”iperf_tkiniter.py”というコード名とする）をコピー
1. ”pyinstaller iperf_tkiniter.py”を実行
1. ”dist\iperf_tkiniter.py\iperf_tkiniter.exe”が完成
1. ”wexpect”と”iperf_tkiniter”を含んだdistを配布（利用）対象とする

それでも、"--onefile"（ファイルを１つにまとめる）は不可であった。

EOF