リアルタイム出力および割り込み対応
その4の改良版。tkinterを使ったコマンドプロンプトもどきプログラムの出力結果をリアルタイムに表示させる、割り込み対応とする、、が今回の趣旨。
手段
いろいろ試したのであるが、リアルタイム出力と割り込み対応を同時に行うには、(その8:tkinter&Subprocessでの割り込み)の内容を用いて、Subprocessをスレッド化し、その結果を1行ずつボックス(Text Widget)に出力することで実現できた。
実際の結果
本稿の背景には、”ping”の無限実行をリアルタイムに見る、その実行を”Ctrl-C”入力に中断する、、というものであった。下記がその状況。
ソースコード
ポイントとなる点を記載。コマンドプロンプトもどきプログラムの実現については、その3を参照
コンソールでの割り込み
import signal
import tkinter as tk
def check():
root.after(500, check)
# Start
signal.signal(signal.SIGINT, lambda x,y : close()) # x,y: Number of signal is 2
root = tk.Tk()
root.geometry('600x550')
root.title('Like command prompt 3')
root.after(500, check) # for easily catch ctrc-c in console (context switch)
- コンソールでの「Ctrl-C」シグナルハンドラの定義(close(後述)):ハンドラは2つの引数を取るので、lambda(無名関数)に”x,y”の2つの記述がある。
- check():コンソールでの「Ctrl-C」入力をキャッチしやすくする。その8参照。
コマンドプロンプトWindow(ボックス:TextWidget)
frm = tk.Frame()
frm.place(x = 40, y = 20)
result = tk.Text(frm, font=("", 10), width=70, height=35, wrap="word")
ysc = tk.Scrollbar(frm, orient=tk.VERTICAL, command=result.yview)
ysc.pack(side=tk.RIGHT, fill="y")
result["yscrollcommand"] = ysc.set
result.insert(tk.END, '> ') # insert prompt('> ')
result.pack()
result.bind('<Return>', func)
result.bind('<Control-c>', ctrlC)
root.protocol("WM_DELETE_WINDOW", close)
root.mainloop()
- 「Return/Enter」キー入力時のコールされる関数:func(後述)
- コマンドプロンプトWindowsで「Ctrl-C」入力時にコールされる関数:ctrlC(後述)
- コマンドプロンプトWindowsで「☓」がクリックされたときにコールされる関数:close(後述)
スレッド
import threading
def func(event):
th = threading.Thread(target=cmdThread, daemon=True)
# daemon=True for RuntimeError: main thread is not in main loop
th.start()
return "break"
- スレッドのコール:cmdThread(後述)
- ”daemon=True”により、残スレッドがデーモンスレッドだけになった時に、プログラム全体を終了できる。なお、True無し時には、上記コメントのエラーが表示される。下記リンク参照。
Subprocess実行スレッド
def cmdThread():
global proc
current = result.get('1.0', tk.END)
lastline = current.rsplit('\n')[-2]
laststr = lastline.split(' ', 1)[1]
cmd_arg = laststr.split(' ')
cmd = cmd_arg[0].lower()
if cmd == "exit": # exit program
root.quit()
return
try:
proc = subprocess.Popen(cmd_arg, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
# "shell=True" prevents ctrl-c in GUI
except: # for internal command like "dir"
# proc = subprocess.Popen(cmd_arg, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
# FileNotFoundError: [WinError 2] 指定されたファイルが見つかりません。
proc = subprocess.Popen(cmd_arg, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
proc.wait()
result.insert(tk.END, '\n')
while True:
output = proc.stdout.readline()
output = output.decode('shift_jis')
if output != '':
result.insert(tk.END, output)
result.see('end')
else:
if proc.poll() is not None:
break
result.insert(tk.END, '\n> ')
result.see('end')
return
- キー入力されたコマンドおよび引数の取得。
- Subprocessによりコマンドの実行(Poepn)。
- 結果を1行ずつ読み込み、ボックス(TextWidget)に書き込み。
- Subprocess終了時に次のコマンドプロンプトをボックスに書き込んでリターン。
- トライしたところ下記のようなことが判明。
- ”shell=True”があると、コマンドプロンプトWindowsでの「Ctrl-C」入力が正しく機能しない。
- ”dir”ような内部コマンド実行には、”shell=True”が必要。そのため、”shell=True”なしのPopenで例外発生時に、再度”shell=True”ありのPopenを実行。
- 上記コメント参照。
各種割り込み時にコールされる関数
def killProcess():
if 'proc' in globals():
proc.terminate()
return
def ctrlC(event):
killProcess()
return
def close():
killProcess()
root.quit()
return
- コマンドプロンプトでの「Ctrl-C」入力時はSubprocessをkill。
- コンソールでの「Ctrl-C」入力時およびコマンドプロンプトWindowsで「☓」クリック時はSubprocessをkillしプログラムを終了。
全体
# -*- coding: utf-8 -*-
import subprocess
import signal
import threading
import time
import tkinter as tk
#
def killProcess():
if 'proc' in globals():
proc.terminate()
return
def ctrlC(event):
killProcess()
return
def close():
killProcess()
root.quit()
return
def cmdThread():
global proc
current = result.get('1.0', tk.END)
lastline = current.rsplit('\n')[-2]
laststr = lastline.split(' ', 1)[1]
cmd_arg = laststr.split(' ')
cmd = cmd_arg[0].lower()
if cmd == "exit": # exit program
root.quit()
return
try:
proc = subprocess.Popen(cmd_arg, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
# "shell=True" prevents ctrl-c in GUI
except: # for internal command like "dir"
#proc = subprocess.Popen(cmd_arg, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
# FileNotFoundError: [WinError 2] 指定されたファイルが見つかりません。
proc = subprocess.Popen(cmd_arg, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
proc.wait()
result.insert(tk.END, '\n')
while True:
output = proc.stdout.readline()
output = output.decode('shift_jis')
if output != '':
result.insert(tk.END, output)
result.see('end')
else:
if proc.poll() is not None:
break
result.insert(tk.END, '\n> ')
result.see('end')
return
def func(event):
th = threading.Thread(target=cmdThread, daemon=True)
# daemon=True for RuntimeError: main thread is not in main loop
th.start()
return "break"
def check():
root.after(500, check)
# Start
signal.signal(signal.SIGINT, lambda x,y : close()) # x,y: Number of signal is 2
root = tk.Tk()
root.geometry('600x550')
root.title('Like command prompt 3')
root.after(500, check) # for easily catch ctrc-c in console (context switch)
frm = tk.Frame()
frm.place(x = 40, y = 20)
result = tk.Text(frm, font=("", 10), width=70, height=35, wrap="word")
ysc = tk.Scrollbar(frm, orient=tk.VERTICAL, command=result.yview)
ysc.pack(side=tk.RIGHT, fill="y")
result["yscrollcommand"] = ysc.set
result.insert(tk.END, '> ') # insert prompt('> ')
result.pack()
result.bind('<Return>', func)
result.bind('<Control-c>', ctrlC)
root.protocol("WM_DELETE_WINDOW", close)
root.mainloop()
EOF