ろうとるがPythonを扱う、、（その９：まとも版コマンドプロンプトもどき改良版）

# リアルタイム出力および割り込み対応
[その４](https://qiita.com/infinite1oop/items/fd3793325c5999119b16)の改良版。tkinterを使ったコマンドプロンプトもどきプログラムの出力結果をリアルタイムに表示させる、割り込み対応とする、、が今回の趣旨。

## 手段
いろいろ試したのであるが、リアルタイム出力と割り込み対応を同時に行うには、[（その８：tkinter&Subprocessでの割り込み）](https://qiita.com/infinite1oop/items/ec44bed5a2964a003b7a)の内容を用いて、Subprocessをスレッド化し、その結果を１行ずつボックス（Text Widget）に出力することで実現できた。

## 実際の結果
本稿の背景には、”ping”の無限実行をリアルタイムに見る、その実行を”Ctrl-C”入力に中断する、、というものであった。下記がその状況。
![スクリーンショット 2022-11-12 20.42.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/d081be3f-0b85-e527-31b0-b91b8b1439d4.png)

## ソースコード
ポイントとなる点を記載。コマンドプロンプトもどきプログラムの実現については、[その３](https://qiita.com/infinite1oop/items/96a153037bd7b6edf093)を参照

### コンソールでの割り込み
```python
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（後述））：ハンドラは２つの引数を取るので、lambda（無名関数）に”x,y”の２つの記述がある。
- check()：コンソールでの「Ctrl-C」入力をキャッチしやすくする。[その８](https://qiita.com/infinite1oop/items/ec44bed5a2964a003b7a)参照。

### コマンドプロンプトWindow（ボックス：TextWidget）
```python
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（後述）

### スレッド
```python
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無し時には、上記コメントのエラーが表示される。下記リンク参照。
    - [Pythonスクリプト【スレッドを使おう③～daemon】](https://mulberrytassel.com/python-practice-thread-3/)
    - [threading --- スレッドベースの並列処理](https://docs.python.org/ja/3.7/library/threading.html) 

### Subprocess実行スレッド
```python
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）。
- 結果を１行ずつ読み込み、ボックス（TextWidget）に書き込み。
- Subprocess終了時に次のコマンドプロンプトをボックスに書き込んでリターン。
- トライしたところ下記のようなことが判明。
    - ”shell=True”があると、コマンドプロンプトWindowsでの「Ctrl-C」入力が正しく機能しない。
    - ”dir”ような内部コマンド実行には、”shell=True”が必要。そのため、”shell=True”なしのPopenで例外発生時に、再度”shell=True”ありのPopenを実行。
    - 上記コメント参照。

### 各種割り込み時にコールされる関数
```python
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しプログラムを終了。

### 全体
```python
# -*- 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