PythonでQEMUモニタを操作する

# 概要
QEMUモニタの操作（メモリダンプ等）を自動化する方法を考えていたところ、Pythonの`subprocess`モジュールを使用すれば実現できそうだったので実際に試してみました。

とりあえずの目標として、QEMUモニタをインタラクティブに操作するPythonスクリプトを作成します。

# QEMUの起動
```python:qemu_test.py
import subprocess

kernel_path = （イメージファイルへの絶対パス）
start_cmd = ['qemu-system-aarch64', '-cpu', 'cortex-a57', '-machine', 'virt', 
			'-kernel', kernel_path, '-monitor', 'stdio', '-nographic']
proc = subprocess.Popen(start_cmd, stdin=subprocess.PIPE,
						stdout=subprocess.PIPE, text=True, encoding='utf-8')
```
`subprocess.Popen`クラスを利用してQEMUを起動します。

QEMU起動用のコマンドに色々オプションがついていますが、標準入出力を通してQEMUモニタを制御するための`-monitor stdio`オプションは必須です。

また、標準入出力をwrite/readで読み書きするため、`Popen`コンストラクタの引数stdin, stdoutにはそれぞれ`subprocess.PIPE`を指定します。



# QEMUモニタの操作
`Popen`の各種メソッドを用いて、QEMUモニタをインタラクティブに操作する方法を検討します。

## 標準入力への書き込み
標準入力への書き込みは以下のコードで実現できます。
```python:qemu_test.py
cmd = "入力するコマンド"

proc.stdin.write(cmd+'\n')
proc.stdin.flush()
```
`Popen.stdin.flush()`を行わないと書き込みが標準入力へ反映されないそうなので、注意が必要です。


## 標準出力の読み込み
標準出力の読み込み処理の実装がやや面倒だったので、順を追って説明します。

### 問題点
通常、シェルにコマンドを入力してQEMUを起動した場合は以下のように出力されます。
```:bash
QEMU 6.2.0 monitor - type 'help' for more information
(qemu)
```
調査の結果、上記の (qemu) プロンプトを`Popen.stdout.readline()`で読み込もうとすると、
**何らかのコマンドを入力しない限り**ハングアップしてしまうことが判明しました。

したがって、
- `Popen.stdout.readline()`で標準出力を逐次読み出そうとすると、 (qemu) プロンプトを読み出そうとした時点でスクリプトがハングアップする
- 一方、 (qemu) プロンプトの直前まで標準出力を読み出そうにも、 (qemu) プロンプトの**直前かどうかを判断する手立てが無い**

といった理由から、入力したコマンドの結果をインタラクティブに取得するのが難しいという問題点があります。

### 解決策
強引な解決策ですが、QEMUモニタが基本的にステートレスであるのを利用して、何らかのコマンドをQEMUモニタへ入力したのちに**無効なコマンド**（例：hoge）を入力する処理を挟むことで解決しました。

QEMUモニタは無効なコマンドを入力された場合`unknown command: '（コマンド）'`と出力するので、`unknown command`を含んだ文字列を読み込んだ時点で`Popen.stdout.readline()`による読み込みを停止することで、ハングアップを回避しつつ (qemu) プロンプトの直前までの標準出力の読み出しを実現できます。

# コード全容
最終的には以下のようなコードになりました。
```python:qemu_test.py
import subprocess

class QemuMonitor():
	def __init__(self):
		self.kernel_path = （イメージファイルへの絶対パス）
		self.start_cmd = ['qemu-system-aarch64', '-cpu', 'cortex-a57', '-machine', 'virt', 
						'-kernel', self.kernel_path, '-monitor', 'stdio', '-nographic']
		self.proc = subprocess.Popen(self.start_cmd, stdin=subprocess.PIPE, 
									stdout=subprocess.PIPE, text=True, encoding='utf-8')

		self._prepare_for_interactive()

	def _prepare_for_interactive(self):
		self.proc.stdout.readline()

	def talk(self, cmd=''):
		if cmd == '':
			return 1
		self.proc.stdin.write(cmd+'\n')
		self.proc.stdin.flush()
		if self.is_alive():
			self.proc.stdin.write("hoge\n")
			self.proc.stdin.flush()
		count = 0
		while(self.is_alive()):
			line = self.proc.stdout.readline()
			count += 1
			if 'unknown command' in line:
				if count == 2:
					print(line, end="")
					continue
				else:
					break
			if '(qemu)' in line:
				continue
			print(line, end="")

	def is_alive(self):
		if self.proc.poll() == None:
			return True
		else:
			return False

	def kill_qemu(self):
		if self.is_alive() == True:
			self.proc.send_signal(2)
		
	def __del__(self):
		self.kill_qemu()

if __name__ == "__main__":
	qm = QemuMonitor()
	while(qm.is_alive()):
		print("(qemu) ", end="")
		cmd = input()
		qm.talk(cmd)
```

このスクリプトによって、「QEMUモニタをインタラクティブに操作する」という一応の目標は達成できました。

QEMUモニタを極力再現するために手を加えた結果汚いコードになっていますが、リファクタリングは今後の課題とします。

このスクリプトを流用すれば、「QEMUモニタの処理を自動化する」という当初の目的は達成できそうです。

# 所感
Pythonの`subprocess`モジュールを使用すれば、QEMUモニタの処理を自動化できることが分かりました。

QEMUモニタだけでなく、インタラクティブな入出力を行う様々なプログラムの自動化にも応用が効くと思われます。

# 参考サイト
[subprocess --- サブプロセス管理 — Python 3.10.4 ドキュメント](https://docs.python.org/ja/3/library/subprocess.html#module-subprocess)
[Popenでinteractiveシェルと通信する | tm23forest.com](https://tm23forest.com/contents/popen-interactive-communication)